1 /*
2 * Copyright (c) 2022, 2026, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23
24 /**
25 * ValueObjectCompilationTests
26 *
27 * @test
28 * @bug 8287136 8292630 8279368 8287136 8287770 8279840 8279672 8292753 8287763 8279901 8287767 8293183 8293120
29 * 8329345 8341061 8340984 8334484
30 * @summary Negative compilation tests, and positive compilation (smoke) tests for Value Objects
31 * @enablePreview
32 * @library /lib/combo /tools/lib
33 * @modules
34 * jdk.compiler/com.sun.tools.javac.util
35 * jdk.compiler/com.sun.tools.javac.api
36 * jdk.compiler/com.sun.tools.javac.main
37 * jdk.compiler/com.sun.tools.javac.code
38 * @build toolbox.ToolBox toolbox.JavacTask
39 * @run junit ValueObjectCompilationTests
40 */
41
42 import java.io.File;
43
44 import java.lang.classfile.Attributes;
45 import java.lang.classfile.ClassFile;
46 import java.lang.classfile.ClassModel;
47 import java.lang.classfile.Instruction;
48 import java.lang.classfile.MethodModel;
49 import java.lang.classfile.Opcode;
50 import java.lang.classfile.instruction.FieldInstruction;
51 import java.lang.constant.ConstantDescs;
52 import java.lang.reflect.AccessFlag;
53 import java.lang.reflect.Method;
54 import java.util.ArrayList;
55 import java.util.List;
56 import java.util.Locale;
57 import java.util.Set;
58
59 import com.sun.tools.javac.util.Assert;
60
61 import com.sun.tools.javac.code.Flags;
62
63 import org.junit.jupiter.api.Test;
64 import tools.javac.combo.CompilationTestCase;
65 import toolbox.ToolBox;
66
67 class ValueObjectCompilationTests extends CompilationTestCase {
68
69 private static String[] PREVIEW_OPTIONS = {
70 "--enable-preview",
71 "-source", Integer.toString(Runtime.version().feature())
72 };
73
74 public ValueObjectCompilationTests() {
75 setDefaultFilename("ValueObjectsTest.java");
76 setCompileOptions(PREVIEW_OPTIONS);
77 }
78
79 @Test
80 void testValueModifierConstraints() {
81 assertFail("compiler.err.illegal.combination.of.modifiers",
82 """
83 value @interface IA {}
84 """);
85 assertFail("compiler.err.illegal.combination.of.modifiers",
86 """
87 value interface I {}
88 """);
89 assertFail("compiler.err.mod.not.allowed.here",
90 """
91 class Test {
92 value int x;
93 }
94 """);
95 assertFail("compiler.err.mod.not.allowed.here",
96 """
97 class Test {
98 value int foo();
99 }
100 """);
101 assertFail("compiler.err.mod.not.allowed.here",
102 """
103 value enum Enum {}
104 """);
105 }
106
107 record TestData(String message, String snippet, String[] compilerOptions, boolean testLocalToo) {
108 TestData(String snippet) {
109 this("", snippet, null, true);
110 }
111
112 TestData(String snippet, boolean testLocalToo) {
113 this("", snippet, null, testLocalToo);
114 }
115
116 TestData(String message, String snippet) {
117 this(message, snippet, null, true);
118 }
119
120 TestData(String snippet, String[] compilerOptions) {
121 this("", snippet, compilerOptions, true);
122 }
123
124 TestData(String message, String snippet, String[] compilerOptions) {
125 this(message, snippet, compilerOptions, true);
126 }
127
128 TestData(String message, String snippet, boolean testLocalToo) {
129 this(message, snippet, null, testLocalToo);
130 }
131 }
132
133 private void testHelper(List<TestData> testDataList) {
134 String ttt =
135 """
136 class TTT {
137 void m() {
138 #LOCAL
139 }
140 }
141 """;
142 for (TestData td : testDataList) {
143 String localSnippet = ttt.replace("#LOCAL", td.snippet);
144 String[] previousOptions = getCompileOptions();
145 try {
146 if (td.compilerOptions != null) {
147 setCompileOptions(td.compilerOptions);
148 }
149 if (td.message == "") {
150 assertOK(td.snippet);
151 if (td.testLocalToo) {
152 assertOK(localSnippet);
153 }
154 } else if (td.message.startsWith("compiler.err")) {
155 assertFail(td.message, td.snippet);
156 if (td.testLocalToo) {
157 assertFail(td.message, localSnippet);
158 }
159 } else {
160 assertOKWithWarning(td.message, td.snippet);
161 if (td.testLocalToo) {
162 assertOKWithWarning(td.message, localSnippet);
163 }
164 }
165 } finally {
166 setCompileOptions(previousOptions);
167 }
168 }
169 }
170
171 private static final List<TestData> superClassConstraints = List.of(
172 new TestData(
173 "compiler.err.value.type.has.identity.super.type",
174 """
175 abstract class I {
176 synchronized void foo() {}
177 }
178 value class V extends I {}
179 """
180 ),
181 new TestData(
182 "compiler.err.value.type.has.identity.super.type",
183 """
184 class ConcreteSuperType {
185 static abstract value class V extends ConcreteSuperType {} // Error: concrete super.
186 }
187 """
188 ),
189 new TestData(
190 """
191 value record Point(int x, int y) {}
192 """
193 ),
194 new TestData(
195 """
196 value class One extends Number {
197 public int intValue() { return 0; }
198 public long longValue() { return 0; }
199 public float floatValue() { return 0; }
200 public double doubleValue() { return 0; }
201 }
202 """
203 ),
204 new TestData(
205 """
206 value class V extends Object {}
207 """
208 ),
209 new TestData(
210 "compiler.err.value.type.has.identity.super.type",
211 """
212 abstract class A {}
213 value class V extends A {}
214 """
215 )
216 );
217
218 @Test
219 void testSuperClassConstraints() {
220 testHelper(superClassConstraints);
221 }
222
223 @Test
224 void testRepeatedModifiers() {
225 assertFail("compiler.err.repeated.modifier", "value value class ValueTest {}");
226 }
227
228 @Test
229 void testParserTest() {
230 assertOK(
231 """
232 value class Substring implements CharSequence {
233 private String str;
234 private int start;
235 private int end;
236
237 public Substring(String str, int start, int end) {
238 checkBounds(start, end, str.length());
239 this.str = str;
240 this.start = start;
241 this.end = end;
242 }
243
244 public int length() {
245 return end - start;
246 }
247
248 public char charAt(int i) {
249 checkBounds(0, i, length());
250 return str.charAt(start + i);
251 }
252
253 public Substring subSequence(int s, int e) {
254 checkBounds(s, e, length());
255 return new Substring(str, start + s, start + e);
256 }
257
258 public String toString() {
259 return str.substring(start, end);
260 }
261
262 private static void checkBounds(int start, int end, int length) {
263 if (start < 0 || end < start || length < end)
264 throw new IndexOutOfBoundsException();
265 }
266 }
267 """
268 );
269 }
270
271 private static final List<TestData> semanticsViolations = List.of(
272 new TestData(
273 "compiler.err.cant.inherit.from.final",
274 """
275 value class Base {}
276 class Subclass extends Base {}
277 """
278 ),
279 new TestData(
280 "compiler.err.cant.assign.val.to.var",
281 """
282 value class Point {
283 int x = 10;
284 int y;
285 Point (int x, int y) {
286 this.x = x; // Error, final field 'x' is already assigned to.
287 this.y = y; // OK.
288 }
289 }
290 """
291 ),
292 new TestData(
293 "compiler.err.cant.assign.val.to.var",
294 """
295 value class Point {
296 int x;
297 int y;
298 Point (int x, int y) {
299 this.x = x;
300 this.y = y;
301 }
302 void foo(Point p) {
303 this.y = p.y; // Error, y is final and can't be written outside of ctor.
304 }
305 }
306 """
307 ),
308 new TestData(
309 "compiler.err.cant.assign.val.to.var",
310 """
311 abstract value class Point {
312 int x;
313 int y;
314 Point (int x, int y) {
315 this.x = x;
316 this.y = y;
317 }
318 void foo(Point p) {
319 this.y = p.y; // Error, y is final and can't be written outside of ctor.
320 }
321 }
322 """
323 ),
324 new TestData(
325 "compiler.err.strict.field.not.have.been.initialized.before.super",
326 """
327 value class Point {
328 int x;
329 int y;
330 Point (int x, int y) {
331 this.x = x;
332 // y hasn't been initialized
333 }
334 }
335 """
336 ),
337 new TestData(
338 "compiler.err.mod.not.allowed.here",
339 """
340 abstract value class V {
341 synchronized void foo() {
342 // Error, abstract value class may not declare a synchronized instance method.
343 }
344 }
345 """
346 ),
347 new TestData(
348 """
349 abstract value class V {
350 static synchronized void foo() {} // OK static
351 }
352 """
353 ),
354 new TestData(
355 "compiler.err.mod.not.allowed.here",
356 """
357 value class V {
358 synchronized void foo() {}
359 }
360 """
361 ),
362 new TestData(
363 """
364 value class V {
365 synchronized static void soo() {} // OK static
366 }
367 """
368 ),
369 new TestData(
370 "compiler.err.type.found.req",
371 """
372 value class V {
373 { synchronized(this) {} }
374 }
375 """
376 ),
377 new TestData(
378 "compiler.err.mod.not.allowed.here",
379 """
380 value record R() {
381 synchronized void foo() { } // Error;
382 synchronized static void soo() {} // OK.
383 }
384 """
385 ),
386 new TestData(
387 "compiler.err.cant.ref.before.ctor.called",
388 """
389 value class V {
390 int x;
391 V() {
392 foo(this); // Error.
393 x = 10;
394 }
395 void foo(V v) {}
396 }
397 """
398 ),
399 new TestData(
400 "compiler.err.cant.ref.before.ctor.called",
401 """
402 value class V {
403 int x;
404 V() {
405 x = 10;
406 foo(this); // error
407 }
408 void foo(V v) {}
409 }
410 """
411 ),
412 new TestData(
413 "compiler.err.type.found.req",
414 """
415 interface I {}
416 interface VI extends I {}
417 class C {}
418 value class VC<T extends VC> {
419 void m(T t) {
420 synchronized(t) {} // error
421 }
422 }
423 """
424 ),
425 new TestData(
426 "compiler.err.type.found.req",
427 """
428 interface I {}
429 interface VI extends I {}
430 class C {}
431 value class VC<T extends VC> {
432 void foo(Object o) {
433 synchronized ((VC & I)o) {} // error
434 }
435 }
436 """
437 ),
438 new TestData(
439 // OK if the value class is abstract
440 """
441 interface I {}
442 abstract value class VI implements I {}
443 class C {}
444 value class VC<T extends VC> {
445 void bar(Object o) {
446 synchronized ((VI & I)o) {} // error
447 }
448 }
449 """
450 ),
451 new TestData(
452 "compiler.err.type.found.req", // --enable-preview -source"
453 """
454 class V {
455 final Integer val = Integer.valueOf(42);
456 void test() {
457 synchronized (val) { // error
458 }
459 }
460 }
461 """
462 ),
463 new TestData(
464 "compiler.err.type.found.req", // --enable-preview -source"
465 """
466 import java.time.*;
467 class V {
468 final Duration val = Duration.ZERO;
469 void test() {
470 synchronized (val) { // warn
471 }
472 }
473 }
474 """,
475 false // cant do local as there is an import statement
476 ),
477 new TestData(
478 "compiler.warn.attempt.to.synchronize.on.instance.of.value.based.class", // empty options
479 """
480 class V {
481 final Integer val = Integer.valueOf(42);
482 void test() {
483 synchronized (val) { // warn
484 }
485 }
486 }
487 """,
488 new String[] {}
489 ),
490 new TestData(
491 "compiler.warn.attempt.to.synchronize.on.instance.of.value.based.class", // --source
492 """
493 class V {
494 final Integer val = Integer.valueOf(42);
495 void test() {
496 synchronized (val) { // warn
497 }
498 }
499 }
500 """,
501 new String[] {"--source", Integer.toString(Runtime.version().feature())}
502 ),
503 new TestData(
504 "compiler.warn.attempt.to.synchronize.on.instance.of.value.based.class", // --source
505 """
506 class V {
507 final Integer val = Integer.valueOf(42);
508 void test() {
509 synchronized (val) { // warn
510 }
511 }
512 }
513 """,
514 new String[] {"--source", Integer.toString(Runtime.version().feature())}
515 ),
516 new TestData(
517 "compiler.err.illegal.combination.of.modifiers", // --enable-preview -source"
518 """
519 value class V {
520 volatile int f = 1;
521 }
522 """
523 )
524 );
525
526 @Test
527 void testSemanticsViolations() {
528 testHelper(semanticsViolations);
529 }
530
531 private static final List<TestData> sealedClassesData = List.of(
532 new TestData(
533 """
534 abstract sealed value class SC {}
535 value class VC extends SC {}
536 """,
537 false // local sealed classes are not allowed
538 ),
539 new TestData(
540 """
541 abstract sealed interface SI {}
542 value class VC implements SI {}
543 """,
544 false // local sealed classes are not allowed
545 ),
546 new TestData(
547 """
548 abstract sealed class SC {}
549 final class IC extends SC {}
550 non-sealed class IC2 extends SC {}
551 final class IC3 extends IC2 {}
552 """,
553 false
554 ),
555 new TestData(
556 """
557 abstract sealed interface SI {}
558 final class IC implements SI {}
559 non-sealed class IC2 implements SI {}
560 final class IC3 extends IC2 {}
561 """,
562 false // local sealed classes are not allowed
563 ),
564 new TestData(
565 "compiler.err.non.abstract.value.class.cant.be.sealed.or.non.sealed",
566 """
567 abstract sealed value class SC {}
568 non-sealed value class VC extends SC {}
569 """,
570 false
571 ),
572 new TestData(
573 "compiler.err.non.abstract.value.class.cant.be.sealed.or.non.sealed",
574 """
575 sealed value class SI {}
576 """,
577 false
578 ),
579 new TestData(
580 """
581 sealed abstract value class SI {}
582 value class V extends SI {}
583 """,
584 false
585 ),
586 new TestData(
587 """
588 sealed abstract value class SI permits V {}
589 value class V extends SI {}
590 """,
591 false
592 ),
593 new TestData(
594 """
595 sealed interface I {}
596 non-sealed abstract value class V implements I {}
597 """,
598 false
599 ),
600 new TestData(
601 """
602 sealed interface I permits V {}
603 non-sealed abstract value class V implements I {}
604 """,
605 false
606 )
607 );
608
609 @Test
610 void testInteractionWithSealedClasses() {
611 testHelper(sealedClassesData);
612 }
613
614 @Test
615 void testCheckClassFileFlags() throws Exception {
616 for (String source : List.of(
617 """
618 interface I {}
619 class Test {
620 I i = new I() {};
621 }
622 """,
623 """
624 class C {}
625 class Test {
626 C c = new C() {};
627 }
628 """,
629 """
630 class Test {
631 Object o = new Object() {};
632 }
633 """,
634 """
635 class Test {
636 abstract class Inner {}
637 }
638 """
639 )) {
640 File dir = assertOK(true, source);
641 for (final File fileEntry : dir.listFiles()) {
642 if (fileEntry.getName().contains("$")) {
643 var classFile = ClassFile.of().parse(fileEntry.toPath());
644 Assert.check(classFile.flags().has(AccessFlag.IDENTITY));
645 }
646 }
647 }
648
649 for (String source : List.of(
650 """
651 class C {}
652 """,
653 """
654 abstract class A {
655 int i;
656 }
657 """,
658 """
659 abstract class A {
660 synchronized void m() {}
661 }
662 """,
663 """
664 class C {
665 synchronized void m() {}
666 }
667 """,
668 """
669 abstract class A {
670 int i;
671 { i = 0; }
672 }
673 """,
674 """
675 abstract class A {
676 A(int i) {}
677 }
678 """,
679 """
680 enum E {}
681 """,
682 """
683 record R() {}
684 """
685 )) {
686 File dir = assertOK(true, source);
687 for (final File fileEntry : dir.listFiles()) {
688 var classFile = ClassFile.of().parse(fileEntry.toPath());
689 Assert.check(classFile.flags().has(AccessFlag.IDENTITY));
690 }
691 }
692
693 {
694 String source =
695 """
696 abstract value class A {}
697 value class Sub extends A {} //implicitly final
698 """;
699 File dir = assertOK(true, source);
700 for (final File fileEntry : dir.listFiles()) {
701 var classFile = ClassFile.of().parse(fileEntry.toPath());
702 switch (classFile.thisClass().asInternalName()) {
703 case "Sub":
704 Assert.check((classFile.flags().flagsMask() & (Flags.FINAL)) != 0);
705 break;
706 case "A":
707 Assert.check((classFile.flags().flagsMask() & (Flags.ABSTRACT)) != 0);
708 break;
709 default:
710 throw new AssertionError("you shoulnd't be here");
711 }
712 }
713 }
714
715 for (String source : List.of(
716 """
717 value class V {
718 int i = 0;
719 static int j;
720 }
721 """,
722 """
723 abstract value class A {
724 static int j;
725 }
726
727 value class V extends A {
728 int i = 0;
729 }
730 """
731 )) {
732 File dir = assertOK(true, source);
733 for (final File fileEntry : dir.listFiles()) {
734 var classFile = ClassFile.of().parse(fileEntry.toPath());
735 for (var field : classFile.fields()) {
736 if (!field.flags().has(AccessFlag.STATIC)) {
737 Set<AccessFlag> fieldFlags = field.flags().flags();
738 Assert.check(fieldFlags.size() == 2 && fieldFlags.contains(AccessFlag.FINAL) && fieldFlags.contains(AccessFlag.STRICT_INIT));
739 }
740 }
741 }
742 }
743 }
744
745 @Test
746 void testConstruction() throws Exception {
747 record Data(String src, boolean isRecord) {
748 Data(String src) {
749 this(src, false);
750 }
751 }
752 for (Data data : List.of(
753 new Data(
754 """
755 value class Test {
756 int i = 100;
757 }
758 """),
759 new Data(
760 """
761 value class Test {
762 int i;
763 Test() {
764 i = 100;
765 }
766 }
767 """),
768 new Data(
769 """
770 value class Test {
771 int i;
772 Test() {
773 i = 100;
774 super();
775 }
776 }
777 """),
778 new Data(
779 """
780 value class Test {
781 int i;
782 Test() {
783 this.i = 100;
784 super();
785 }
786 }
787 """),
788 new Data(
789 """
790 value record Test(int i) {}
791 """, true)
792 )) {
793 if (!data.isRecord()) {
794 checkMnemonicsFor(data.src, "aload_0,bipush,putfield,aload_0,invokespecial,return");
795 } else {
796 checkMnemonicsFor(data.src, "aload_0,iload_1,putfield,aload_0,invokespecial,return");
797 }
798 }
799
800 String source =
801 """
802 value class Test {
803 int i = 100;
804 int j = 0;
805 {
806 System.out.println(j);
807 }
808 }
809 """;
810 checkMnemonicsFor(
811 """
812 value class Test {
813 int i = 100;
814 int j = 0;
815 {
816 System.out.println(j);
817 }
818 }
819 """,
820 "aload_0,bipush,putfield,aload_0,iconst_0,putfield,aload_0,invokespecial,getstatic,iconst_0,invokevirtual,return"
821 );
822
823 assertFail("compiler.err.cant.ref.before.ctor.called",
824 """
825 value class Test {
826 Test() {
827 m();
828 }
829 void m() {}
830 }
831 """
832 );
833 assertFail("compiler.err.strict.field.not.have.been.initialized.before.super",
834 """
835 value class Test {
836 int i;
837 Test() {
838 super();
839 this.i = i;
840 }
841 }
842 """
843 );
844 assertOK(
845 """
846 class UnrelatedThisLeak {
847 value class V {
848 int f;
849 V() {
850 UnrelatedThisLeak x = UnrelatedThisLeak.this;
851 f = 10;
852 x = UnrelatedThisLeak.this;
853 }
854 }
855 }
856 """
857 );
858 assertFail("compiler.err.cant.ref.before.ctor.called",
859 """
860 value class Test {
861 Test t = null;
862 Runnable r = () -> { System.err.println(t); }; // cant reference `t` from a lambda expression in the prologue
863 }
864 """
865 );
866 assertFail("compiler.err.strict.field.not.have.been.initialized.before.super",
867 """
868 value class Test {
869 int f;
870 {
871 f = 1;
872 }
873 }
874 """
875 );
876 assertFail("compiler.err.var.might.not.have.been.initialized",
877 """
878 value class V {
879 int x;
880 int y = x + 1; // error
881 V() {
882 x = 12;
883 // super();
884 }
885 }
886 """
887 );
888 assertOK("""
889 value class V {
890 int y;
891 int x = (y = 1);
892
893 V() {
894 int z = y; // ok
895 super();
896 }
897 }
898 """);
899 assertFail("compiler.err.cant.ref.before.ctor.called",
900 """
901 value class V2 {
902 int x;
903 V2() { this(x = 3); } // error
904 V2(int i) { x = 4; }
905 }
906 """
907 );
908 assertOK(
909 """
910 abstract value class AV1 {
911 AV1(int i) {}
912 }
913 value class V3 extends AV1 {
914 int x;
915 V3() {
916 super(x = 3); // ok
917 }
918 }
919 """
920 );
921 assertFail("compiler.err.var.might.not.have.been.initialized",
922 """
923 value class V4 {
924 int x;
925 int y = x + 1;
926 V4() {
927 x = 12;
928 }
929 V4(int i) {
930 x = i;
931 }
932 }
933 """
934 );
935 assertOK(
936 """
937 value class V {
938 final int x = "abc".length();
939 { System.out.println(x); }
940 }
941 """
942 );
943 assertFail("compiler.err.illegal.forward.ref",
944 """
945 value class V {
946 { System.out.println(x); }
947 final int x = "abc".length();
948 }
949 """
950 );
951 assertOK(
952 """
953 value class V {
954 int x = "abc".length();
955 int y = x;
956 }
957 """
958 );
959 assertOK(
960 """
961 value class V {
962 int x = "abc".length();
963 { int y = x; }
964 }
965 """
966 );
967 assertOK(
968 """
969 value class V {
970 String s1;
971 { System.out.println(s1); }
972 String s2 = (s1 = "abc");
973 }
974 """
975 );
976
977 source =
978 """
979 value class V {
980 int i = 1;
981 int y;
982 V() {
983 y = 2;
984 }
985 }
986 """;
987 {
988 File dir = assertOK(true, source);
989 File fileEntry = dir.listFiles()[0];
990 var expectedCodeSequence = "putfield i,putfield y";
991 var classFile = ClassFile.of().parse(fileEntry.toPath());
992 for (var method : classFile.methods()) {
993 if (method.methodName().equalsString("<init>")) {
994 var code = method.findAttribute(Attributes.code()).orElseThrow();
995 List<String> mnemonics = new ArrayList<>();
996 for (var coe : code) {
997 if (coe instanceof FieldInstruction inst && inst.opcode() == Opcode.PUTFIELD) {
998 mnemonics.add(inst.opcode().name().toLowerCase(Locale.ROOT) + " " + inst.name());
999 }
1000 }
1001 var foundCodeSequence = String.join(",", mnemonics);
1002 Assert.check(expectedCodeSequence.equals(foundCodeSequence), "found " + foundCodeSequence);
1003 }
1004 }
1005 }
1006
1007 // check that javac doesn't generate duplicate initializer code
1008 checkMnemonicsFor(
1009 """
1010 value class Test {
1011 static class Foo {
1012 int x;
1013 int getX() { return x; }
1014 }
1015 Foo data = new Foo();
1016 Test() { // we will check that: `data = new Foo();` is generated only once
1017 data.getX();
1018 super();
1019 }
1020 }
1021 """,
1022 "new,dup,invokespecial,astore_1,aload_1,invokevirtual,pop,aload_0,aload_1,putfield,aload_0,invokespecial,return"
1023 );
1024
1025 assertFail("compiler.err.invalid.canonical.constructor.in.record",
1026 """
1027 record R(int x) {
1028 public R {
1029 super();
1030 }
1031 }
1032 """
1033 );
1034
1035 assertFail("compiler.err.invalid.canonical.constructor.in.record",
1036 """
1037 record R(int x) {
1038 public R {
1039 this();
1040 }
1041 public R() {
1042 this(1);
1043 }
1044 }
1045 """
1046 );
1047
1048 assertOK(
1049 """
1050 record R(int x) {
1051 public R(int x) {
1052 this.x = x;
1053 super();
1054 }
1055 }
1056 """
1057 );
1058 }
1059
1060 @Test
1061 void testSyntheticCapturesInEarlyInitializers() throws Exception {
1062 File dir = assertOK(true, """
1063 class Test {
1064 class Inner { }
1065 value class V {
1066 Object o = new Inner(); // this$0 ref
1067 V() { super(); }
1068 }
1069 public static void main(String[] args) {
1070 Test t = new Test();
1071 t.new V();
1072 }
1073 }
1074 """);
1075 invokeMain("Test", dir);
1076
1077 dir = assertOK(true, """
1078 class Test {
1079 static class Box { Box(int i) { } }
1080 static void test() {
1081 int x = 42;
1082 value class V {
1083 Object o = new Box(x); // capture ref
1084 V() { super(); }
1085 }
1086 new V();
1087 }
1088 public static void main(String[] args) {
1089 test();
1090 }
1091 }
1092 """);
1093 invokeMain("Test", dir);
1094
1095 dir = assertOK(true, """
1096 import java.util.function.Supplier;
1097
1098 class Test {
1099 static int seen;
1100 static class Box { Box(int i) { } }
1101 static void test() {
1102 int x = 42;
1103 value class V {
1104 Supplier<Box> s = () -> new Box(x); // lambda capture ref
1105 V() { super(); }
1106 }
1107 new V().s.get();
1108 }
1109 public static void main(String[] args) {
1110 test();
1111 }
1112 }
1113 """);
1114 invokeMain("Test", dir);
1115
1116 dir = assertOK(true, """
1117 import java.util.function.Supplier;
1118
1119 class Test {
1120 static int seen;
1121 class Inner { }
1122 value class V {
1123 Supplier<Inner> s = () -> new Inner(); // lambda this$0 ref
1124 V() { super(); }
1125 }
1126 public static void main(String[] args) {
1127 Test t = new Test();
1128 t.new V().s.get();
1129 }
1130 }
1131 """);
1132 invokeMain("Test", dir);
1133 }
1134
1135 void invokeMain(String className, File dir) throws Exception {
1136 Method method = loadClass(className, dir)
1137 .getDeclaredMethod("main", String[].class);
1138 method.setAccessible(true);
1139 method.invoke(null, (Object) new String[0]);
1140 }
1141
1142 void checkMnemonicsFor(String source, String expectedMnemonics) throws Exception {
1143 File dir = assertOK(true, source);
1144 for (final File fileEntry : dir.listFiles()) {
1145 var classFile = ClassFile.of().parse(fileEntry.toPath());
1146 if (classFile.thisClass().name().equalsString("Test")) {
1147 for (var method : classFile.methods()) {
1148 if (method.methodName().equalsString("<init>")) {
1149 var code = method.findAttribute(Attributes.code()).orElseThrow();
1150 List<String> mnemonics = new ArrayList<>();
1151 for (var coe : code) {
1152 if (coe instanceof Instruction inst) {
1153 mnemonics.add(inst.opcode().name().toLowerCase(Locale.ROOT));
1154 }
1155 }
1156 var foundCodeSequence = String.join(",", mnemonics);
1157 Assert.check(expectedMnemonics.equals(foundCodeSequence), "found " + foundCodeSequence);
1158 }
1159 }
1160 }
1161 }
1162 }
1163
1164 @Test
1165 void testThisCallingConstructor() throws Exception {
1166 // make sure that this() calling constructors doesn't initialize final fields
1167 String source =
1168 """
1169 value class Test {
1170 int i;
1171 Test() {
1172 this(0);
1173 }
1174
1175 Test(int i) {
1176 this.i = i;
1177 }
1178 }
1179 """;
1180 File dir = assertOK(true, source);
1181 File fileEntry = dir.listFiles()[0];
1182 String expectedCodeSequenceThisCallingConst = "aload_0,iconst_0,invokespecial,return";
1183 String expectedCodeSequenceNonThisCallingConst = "aload_0,iload_1,putfield,aload_0,invokespecial,return";
1184 var classFile = ClassFile.of().parse(fileEntry.toPath());
1185 for (var method : classFile.methods()) {
1186 if (method.methodName().equalsString("<init>")) {
1187 var code = method.findAttribute(Attributes.code()).orElseThrow();
1188 List<String> mnemonics = new ArrayList<>();
1189 for (var coe : code) {
1190 if (coe instanceof Instruction inst) {
1191 mnemonics.add(inst.opcode().name().toLowerCase(Locale.ROOT));
1192 }
1193 }
1194 var foundCodeSequence = String.join(",", mnemonics);
1195 var expected = method.methodTypeSymbol().parameterCount() == 0 ?
1196 expectedCodeSequenceThisCallingConst : expectedCodeSequenceNonThisCallingConst;
1197 Assert.check(expected.equals(foundCodeSequence), "found " + foundCodeSequence);
1198 }
1199 }
1200 }
1201
1202 @Test
1203 void testSelectors() throws Exception {
1204 assertOK(
1205 """
1206 value class V {
1207 void selector() {
1208 Class<?> c = int.class;
1209 }
1210 }
1211 """
1212 );
1213 assertFail("compiler.err.expected",
1214 """
1215 value class V {
1216 void selector() {
1217 int i = int.some_selector;
1218 }
1219 }
1220 """
1221 );
1222 }
1223
1224 @Test
1225 void testNullAssigment() throws Exception {
1226 assertOK(
1227 """
1228 value final class V {
1229 final int x = 10;
1230
1231 value final class X {
1232 final V v;
1233 final V v2;
1234
1235 X() {
1236 this.v = null;
1237 this.v2 = null;
1238 }
1239
1240 X(V v) {
1241 this.v = v;
1242 this.v2 = v;
1243 }
1244
1245 V foo(X x) {
1246 x = new X(null); // OK
1247 return x.v;
1248 }
1249 }
1250 V bar(X x) {
1251 x = new X(null); // OK
1252 return x.v;
1253 }
1254
1255 class Y {
1256 V v;
1257 V [] va = { null }; // OK: array initialization
1258 V [] va2 = new V[] { null }; // OK: array initialization
1259 void ooo(X x) {
1260 x = new X(null); // OK
1261 v = null; // legal assignment.
1262 va[0] = null; // legal.
1263 va = new V[] { null }; // legal
1264 }
1265 }
1266 }
1267 """
1268 );
1269 }
1270
1271 @Test
1272 void testSerializationWarnings() throws Exception {
1273 String[] previousOptions = getCompileOptions();
1274 try {
1275 setCompileOptions(new String[] {"-Xlint:serial", "--enable-preview", "--source",
1276 Integer.toString(Runtime.version().feature())});
1277 assertOK(
1278 """
1279 import java.io.*;
1280 abstract value class AVC implements Serializable {}
1281 """);
1282 assertOKWithWarning("compiler.warn.serializable.value.class.without.write.replace.1",
1283 """
1284 import java.io.*;
1285 value class VC implements Serializable {
1286 private static final long serialVersionUID = 0;
1287 }
1288 """);
1289 assertOKWithWarning("compiler.warn.ineffectual.serial.method.value.class",
1290 """
1291 import java.io.*;
1292 value class VC implements Serializable {
1293 private static final long serialVersionUID = 0;
1294 private void writeObject(ObjectOutputStream stream) throws IOException {}
1295 }
1296 """);
1297 assertOKWithWarning("compiler.warn.ineffectual.serial.method.value.class",
1298 """
1299 import java.io.*;
1300 value class VC implements Serializable {
1301 private static final long serialVersionUID = 0;
1302 private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {}
1303 }
1304 """);
1305 assertOKWithWarning("compiler.warn.ineffectual.serial.method.value.class",
1306 """
1307 import java.io.*;
1308 value class VC implements Serializable {
1309 private static final long serialVersionUID = 0;
1310 private void readObjectNoData() throws ObjectStreamException {}
1311 }
1312 """);
1313 assertOK(
1314 """
1315 import java.io.*;
1316 class C implements Serializable {
1317 private static final long serialVersionUID = 0;
1318 }
1319 """);
1320 assertOK(
1321 """
1322 import java.io.*;
1323 abstract value class Super implements Serializable {
1324 private static final long serialVersionUID = 0;
1325 protected Object writeReplace() throws ObjectStreamException {
1326 return null;
1327 }
1328 }
1329 value class ValueSerializable extends Super {
1330 private static final long serialVersionUID = 1;
1331 }
1332 """);
1333 assertOK(
1334 """
1335 import java.io.*;
1336 abstract value class Super implements Serializable {
1337 private static final long serialVersionUID = 0;
1338 Object writeReplace() throws ObjectStreamException {
1339 return null;
1340 }
1341 }
1342 value class ValueSerializable extends Super {
1343 private static final long serialVersionUID = 1;
1344 }
1345 """);
1346 assertOK(
1347 """
1348 import java.io.*;
1349 abstract value class Super implements Serializable {
1350 private static final long serialVersionUID = 0;
1351 public Object writeReplace() throws ObjectStreamException {
1352 return null;
1353 }
1354 }
1355 value class ValueSerializable extends Super {
1356 private static final long serialVersionUID = 1;
1357 }
1358 """);
1359 assertOKWithWarning("compiler.warn.serializable.value.class.without.write.replace.1",
1360 """
1361 import java.io.*;
1362 abstract value class Super implements Serializable {
1363 private static final long serialVersionUID = 0;
1364 private Object writeReplace() throws ObjectStreamException {
1365 return null;
1366 }
1367 }
1368 value class ValueSerializable extends Super {
1369 private static final long serialVersionUID = 1;
1370 }
1371 """);
1372 assertOKWithWarning("compiler.warn.serializable.value.class.without.write.replace.2",
1373 """
1374 import java.io.*;
1375 abstract value class Super implements Serializable {
1376 private static final long serialVersionUID = 0;
1377 private Object writeReplace() throws ObjectStreamException {
1378 return null;
1379 }
1380 }
1381 class Serializable1 extends Super {
1382 private static final long serialVersionUID = 1;
1383 }
1384 class Serializable2 extends Serializable1 {
1385 private static final long serialVersionUID = 1;
1386 }
1387 """);
1388 assertOK(
1389 """
1390 import java.io.*;
1391 abstract value class Super implements Serializable {
1392 private static final long serialVersionUID = 0;
1393 Object writeReplace() throws ObjectStreamException {
1394 return null;
1395 }
1396 }
1397 class ValueSerializable extends Super {
1398 private static final long serialVersionUID = 1;
1399 }
1400 """);
1401 assertOK(
1402 """
1403 import java.io.*;
1404 abstract value class Super implements Serializable {
1405 private static final long serialVersionUID = 0;
1406 public Object writeReplace() throws ObjectStreamException {
1407 return null;
1408 }
1409 }
1410 class ValueSerializable extends Super {
1411 private static final long serialVersionUID = 1;
1412 }
1413 """);
1414 assertOK(
1415 """
1416 import java.io.*;
1417 abstract value class Super implements Serializable {
1418 private static final long serialVersionUID = 0;
1419 protected Object writeReplace() throws ObjectStreamException {
1420 return null;
1421 }
1422 }
1423 class ValueSerializable extends Super {
1424 private static final long serialVersionUID = 1;
1425 }
1426 """);
1427 assertOK(
1428 """
1429 import java.io.*;
1430 value record ValueRecord() implements Serializable {
1431 private static final long serialVersionUID = 1;
1432 }
1433 """);
1434 assertOK(
1435 // Number is a special case, no warning for identity classes extending it
1436 """
1437 class NumberSubClass extends Number {
1438 private static final long serialVersionUID = 0L;
1439 @Override
1440 public double doubleValue() { return 0; }
1441 @Override
1442 public int intValue() { return 0; }
1443 @Override
1444 public long longValue() { return 0; }
1445 @Override
1446 public float floatValue() { return 0; }
1447 }
1448 """
1449 );
1450 } finally {
1451 setCompileOptions(previousOptions);
1452 }
1453 }
1454
1455 @Test
1456 void testAssertUnsetFieldsSMEntry() throws Exception {
1457 String[] previousOptions = getCompileOptions();
1458 try {
1459 String[] testOptions = {
1460 "--enable-preview",
1461 "-source", Integer.toString(Runtime.version().feature()),
1462 "-XDnoLocalProxyVars",
1463 "-XDdebug.stackmap",
1464 };
1465 setCompileOptions(testOptions);
1466
1467 record Data(String src, int[] expectedFrameTypes, String[][] expectedUnsetFields) {}
1468 for (Data data : List.of(
1469 new Data(
1470 """
1471 value class Test {
1472 final int x;
1473 final int y;
1474 Test(boolean a, boolean b) {
1475 if (a) { // early_larval {x, y}
1476 x = 1;
1477 if (b) { // early_larval {y}
1478 y = 1;
1479 } else { // early_larval {y}
1480 y = 2;
1481 }
1482 } else { // early_larval {x, y}
1483 x = y = 3;
1484 }
1485 super();
1486 }
1487 }
1488 """,
1489 // three unset_fields entries, entry type 246, are expected in the stackmap table
1490 new int[] {246, 246, 246},
1491 // expected fields for each of them:
1492 new String[][] { new String[] { "y:I" }, new String[] { "x:I", "y:I" }, new String[] {} }
1493 ),
1494 new Data(
1495 """
1496 value class Test {
1497 final int x;
1498 final int y;
1499 Test(int n) {
1500 switch(n) {
1501 case 2:
1502 x = y = 2;
1503 break;
1504 default:
1505 x = y = 100;
1506 break;
1507 }
1508 super();
1509 }
1510 }
1511 """,
1512 // here we expect only one
1513 new int[] {20, 12, 246},
1514 // stating that no field is unset
1515 new String[][] { new String[] {} }
1516 ),
1517 new Data(
1518 """
1519 value class Test {
1520 final int x;
1521 final int y;
1522 Test(int n) {
1523 if (n % 3 == 0) {
1524 x = n / 3;
1525 } else { // no unset change
1526 x = n + 2;
1527 } // early_larval {y}
1528 y = n >>> 3;
1529 super();
1530 if ((char) n != n) {
1531 n -= 5;
1532 } // no uninitializedThis - automatically cleared unsets
1533 Math.abs(n);
1534 }
1535 }
1536 """,
1537 // here we expect only one, none for the post-larval frame
1538 new int[] {16, 246, 255},
1539 // stating that y is unset when if-else finishes
1540 new String[][] { new String[] {"y:I"} }
1541 )
1542 )) {
1543 File dir = assertOK(true, data.src());
1544 for (final File fileEntry : dir.listFiles()) {
1545 var classFile = ClassFile.of().parse(fileEntry.toPath());
1546 for (var method : classFile.methods()) {
1547 if (method.methodName().equalsString(ConstantDescs.INIT_NAME)) {
1548 var code = method.findAttribute(Attributes.code()).orElseThrow();
1549 var stackMapTable = code.findAttribute(Attributes.stackMapTable()).orElseThrow();
1550 Assert.check(data.expectedFrameTypes().length == stackMapTable.entries().size(), "unexpected stackmap length");
1551 int entryIndex = 0;
1552 int expectedUnsetFieldsIndex = 0;
1553 for (var entry : stackMapTable.entries()) {
1554 Assert.check(data.expectedFrameTypes()[entryIndex++] == entry.frameType(), "expected " + data.expectedFrameTypes()[entryIndex - 1] + " found " + entry.frameType());
1555 if (entry.frameType() == 246) {
1556 Assert.check(data.expectedUnsetFields()[expectedUnsetFieldsIndex].length == entry.unsetFields().size());
1557 int index = 0;
1558 for (var nat : entry.unsetFields()) {
1559 String unsetStr = nat.name() + ":" + nat.type();
1560 Assert.check(data.expectedUnsetFields()[expectedUnsetFieldsIndex][index++].equals(unsetStr));
1561 }
1562 expectedUnsetFieldsIndex++;
1563 }
1564 }
1565 }
1566 }
1567 }
1568 }
1569 } finally {
1570 setCompileOptions(previousOptions);
1571 }
1572 }
1573
1574 @Test
1575 void testLocalProxyVars() throws Exception {
1576 checkMnemonicsFor(
1577 """
1578 value class Test {
1579 int i;
1580 int j;
1581 Test() {// javac should generate a proxy local var for `i`
1582 i = 1;
1583 j = i; // as here `i` is being read during the early construction phase, use the local var instead
1584 super();
1585 }
1586 }
1587 """,
1588 "iconst_1,istore_1,aload_0,iload_1,putfield,aload_0,iload_1,putfield,aload_0,invokespecial,return");
1589 checkMnemonicsFor(
1590 """
1591 value class Test {
1592 static String s0;
1593 String s;
1594 String ss;
1595 Test(boolean b) {
1596 s0 = null;
1597 s = s0; // no local proxy variable for `s0` as it is static
1598 ss = s; // but there should be a local proxy for `s`
1599 super();
1600 }
1601 }
1602 """,
1603 "aconst_null,putstatic,getstatic,astore_2,aload_0,aload_2,putfield,aload_0,aload_2," +
1604 "putfield,aload_0,invokespecial,return");
1605 }
1606
1607 @Test
1608 void testIdentityRecordUsesPreview() throws Exception {
1609 String source_withComponents =
1610 """
1611 record IdentityRecord(int x, int y) {
1612 IdentityRecord(int x, int y) {
1613 this.x = x;
1614 if (x < 0) {
1615 y = -y;
1616 }
1617 this.y = y;
1618 }
1619 }
1620 """;
1621 String source_noComponents =
1622 """
1623 record IdentityRecord() {}
1624 """;
1625
1626 // --enable-preview - use preview VM features
1627 String[] previousOptions = getCompileOptions();
1628 try {
1629 setCompileOptions("--enable-preview",
1630 "-source", Integer.toString(Runtime.version().feature()),
1631 "-Xlint:preview");
1632
1633 File dir = assertOK(true, source_withComponents);
1634 File classFile = new File(dir, "IdentityRecord.class");
1635 Assert.check(classFile.exists(), "missing class file");
1636 var classModel = ClassFile.of().parse(classFile.toPath());
1637 Assert.check(classModel.minorVersion() == ClassFile.PREVIEW_MINOR_VERSION,
1638 "identity records should produce preview class files when compiled with preview enabled");
1639 Assert.check(classModel.fields().stream().allMatch(f -> f.flags().has(AccessFlag.STRICT_INIT)),
1640 "identity record component instance field should be strictly initialized with preview enabled");
1641 var constructor = classModel.methods()
1642 .stream()
1643 .filter(mm -> mm.methodName().equalsString(ConstantDescs.INIT_NAME))
1644 .findFirst()
1645 .orElseThrow();
1646 System.err.println(constructor.toDebugString());
1647 var stackMaps = constructor.code().orElseThrow().findAttribute(Attributes.stackMapTable()).orElseThrow();
1648 Assert.check(stackMaps.entries().getFirst().frameType() == 246,
1649 "identity record constructor StackMapTable should declare unset fields with preview enabled");
1650
1651 dir = assertOK(true, source_noComponents);
1652 classFile = new File(dir, "IdentityRecord.class");
1653 Assert.check(classFile.exists(), "missing class file");
1654 Assert.check(ClassFile.of().parse(classFile.toPath()).minorVersion() == 0,
1655 "identity records with no components should not produce preview class files even with preview enabled");
1656 } finally {
1657 setCompileOptions(previousOptions);
1658 }
1659
1660 // No preview - no preview VM features
1661 previousOptions = getCompileOptions();
1662 try {
1663 setCompileOptions("-source", "28");
1664
1665 File dir = assertOK(true, source_withComponents);
1666 File classFile = new File(dir, "IdentityRecord.class");
1667 Assert.check(classFile.exists(), "missing class file");
1668 var classModel = ClassFile.of().parse(classFile.toPath());
1669 Assert.check(classModel.minorVersion() == 0,
1670 "identity records should not preview class files for older releases");
1671 Assert.check(classModel.fields().stream().noneMatch(f -> f.flags().has(AccessFlag.STRICT_INIT)),
1672 "identity record component instance field should not be strictly initialized for older releases");
1673 var constructor = classModel.methods()
1674 .stream()
1675 .filter(mm -> mm.methodName().equalsString(ConstantDescs.INIT_NAME))
1676 .findFirst()
1677 .orElseThrow();
1678 var stackMaps = constructor.code().orElseThrow().findAttribute(Attributes.stackMapTable()).orElseThrow();
1679 Assert.check(stackMaps.entries().getFirst().frameType() != 246,
1680 "identity record constructor StackMapTable should not declare unset fields for older releases");
1681
1682 dir = assertOK(true, source_noComponents);
1683 classFile = new File(dir, "IdentityRecord.class");
1684 Assert.check(classFile.exists(), "missing class file");
1685 Assert.check(ClassFile.of().parse(classFile.toPath()).minorVersion() == 0,
1686 "identity records with no components should not produce preview class files for older releases");
1687 } finally {
1688 setCompileOptions(previousOptions);
1689 }
1690 }
1691 }