--- a/compiler/src/main/java/org/teavm/javac/Compiler.java +++ b/compiler/src/main/java/org/teavm/javac/Compiler.java @@ -34,22 +34,35 @@ import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Handle; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; import org.teavm.backend.wasm.WasmDebugInfoLocation; import org.teavm.backend.wasm.WasmGCTarget; import org.teavm.classlib.impl.JCLPlugin; +import org.teavm.classlib.java.lang.ClassGenerator; +import org.teavm.classlib.java.lang.ObjectDependencyPlugin; +import org.teavm.classlib.java.lang.StringNativeDependency; +import org.teavm.classlib.java.lang.SystemDependencyPlugin; +import org.teavm.classlib.java.lang.reflect.ArrayDependencyPlugin; import org.teavm.jso.JSClass; import org.teavm.jso.JSExport; import org.teavm.jso.core.JSObjects; import org.teavm.jso.impl.JSOPlugin; import org.teavm.jso.typedarrays.Int8Array; import org.teavm.model.ClassHolderSource; +import org.teavm.model.MethodReference; import org.teavm.model.ReferenceCache; +import org.teavm.model.ValueType; import org.teavm.parsing.ClasspathClassHolderSource; import org.teavm.parsing.CompositeClassHolderSource; import org.teavm.parsing.resource.CompositeResourceProvider; import org.teavm.parsing.resource.ResourceProvider; import org.teavm.parsing.substitution.DefaultSubstituteClassNameMapping; import org.teavm.platform.plugin.PlatformPlugin; +import org.teavm.vm.TeaVM; import org.teavm.vm.TeaVMBuilder; import org.teavm.vm.TeaVMOptimizationLevel; import static com.sun.tools.javac.comp.CompileStates.CompileState; @@ -221,10 +234,146 @@ public boolean compile() { initCompiler(); try { - return compiler.simpleCompile(); + if (!compiler.simpleCompile()) { + return false; + } } finally { compiler = null; } + instrumentOutput(); + return true; + } + + private static final String CHECKS = "gwecode/Checks"; + + private static void emitRecordHash(ClassVisitor cv, String owner, Object[] getters) { + var mv = cv.visitMethod(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_SYNTHETIC, "gw$recordHash", + "(L" + owner + ";)I", null, null); + mv.visitCode(); + mv.visitInsn(Opcodes.ICONST_0); + for (var item : getters) { + var getter = (Handle) item; + mv.visitIntInsn(Opcodes.BIPUSH, 31); + mv.visitInsn(Opcodes.IMUL); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitFieldInsn(Opcodes.GETFIELD, getter.getOwner(), getter.getName(), getter.getDesc()); + switch (getter.getDesc()) { + case "I", "C", "S", "B" -> { } + case "J" -> { + mv.visitInsn(Opcodes.DUP2); + mv.visitIntInsn(Opcodes.BIPUSH, 32); + mv.visitInsn(Opcodes.LUSHR); + mv.visitInsn(Opcodes.LXOR); + mv.visitInsn(Opcodes.L2I); + } + case "D" -> mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Double", "hashCode", "(D)I", false); + case "F" -> mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Float", "hashCode", "(F)I", false); + case "Z" -> mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Boolean", "hashCode", "(Z)I", false); + default -> mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/util/Objects", "hashCode", + "(Ljava/lang/Object;)I", false); + } + mv.visitInsn(Opcodes.IADD); + } + mv.visitInsn(Opcodes.IRETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + + private void instrumentOutput() { + for (var file : outputFiles.values()) { + if (!file.path.endsWith(".class")) { + continue; + } + var reader = new ClassReader(file.data); + var writer = new ClassWriter(ClassWriter.COMPUTE_MAXS) { + @Override + protected String getCommonSuperClass(String type1, String type2) { + return "java/lang/Object"; + } + }; + reader.accept(new ClassVisitor(Opcodes.ASM9, writer) { + private String owner; + private Object[] recordGetters; + + @Override + public void visit(int version, int access, String name, String signature, String superName, + String[] interfaces) { + owner = name; + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public void visitEnd() { + if (recordGetters != null) { + emitRecordHash(cv, owner, recordGetters); + } + super.visitEnd(); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, + String[] exceptions) { + var next = super.visitMethod(access, name, descriptor, signature, exceptions); + return new MethodVisitor(Opcodes.ASM9, next) { + private void check(String method, String desc) { + super.visitMethodInsn(Opcodes.INVOKESTATIC, CHECKS, method, desc, false); + } + + @Override + public void visitInvokeDynamicInsn(String name, String descriptor, Handle bsm, + Object... args) { + if (name.equals("hashCode") && bsm.getOwner().equals("java/lang/runtime/ObjectMethods") + && args.length >= 2 && descriptor.equals("(L" + owner + ";)I")) { + recordGetters = java.util.Arrays.copyOfRange(args, 2, args.length); + super.visitMethodInsn(Opcodes.INVOKESTATIC, owner, "gw$recordHash", descriptor, + false); + return; + } + super.visitInvokeDynamicInsn(name, descriptor, bsm, args); + } + + @Override + public void visitInsn(int opcode) { + switch (opcode) { + case Opcodes.IDIV -> check("idiv", "(II)I"); + case Opcodes.IREM -> check("irem", "(II)I"); + case Opcodes.LDIV -> check("ldiv", "(JJ)J"); + case Opcodes.LREM -> check("lrem", "(JJ)J"); + case Opcodes.IALOAD -> check("load", "([II)I"); + case Opcodes.LALOAD -> check("load", "([JI)J"); + case Opcodes.FALOAD -> check("load", "([FI)F"); + case Opcodes.DALOAD -> check("load", "([DI)D"); + case Opcodes.CALOAD -> check("load", "([CI)C"); + case Opcodes.SALOAD -> check("load", "([SI)S"); + case Opcodes.IASTORE -> check("store", "([III)V"); + case Opcodes.LASTORE -> check("store", "([JIJ)V"); + case Opcodes.FASTORE -> check("store", "([FIF)V"); + case Opcodes.DASTORE -> check("store", "([DID)V"); + case Opcodes.CASTORE -> check("store", "([CIC)V"); + case Opcodes.SASTORE -> check("store", "([SIS)V"); + case Opcodes.AASTORE -> check("store", "([Ljava/lang/Object;ILjava/lang/Object;)V"); + case Opcodes.AALOAD -> { + super.visitInsn(Opcodes.DUP2); + check("index", "([Ljava/lang/Object;I)V"); + super.visitInsn(Opcodes.AALOAD); + } + default -> super.visitInsn(opcode); + } + } + + @Override + public void visitTypeInsn(int opcode, String type) { + super.visitTypeInsn(opcode, type); + if (opcode == Opcodes.CHECKCAST) { + super.visitInsn(Opcodes.DUP); + super.visitFieldInsn(Opcodes.PUTSTATIC, CHECKS, "sink", "Ljava/lang/Object;"); + } + } + }; + } + }, 0); + file.data = writer.toByteArray(); + } } @JSExport @@ -276,10 +425,17 @@ .setObfuscated(obfuscated) .setStrict(strictMode) .build(); - teavm.setOptimizationLevel(TeaVMOptimizationLevel.ADVANCED); - new JSOPlugin().install(teavm); + var level = JSObjects.isUndefined(options.getOptimization()) || options.getOptimization() == null + ? "advanced" : options.getOptimization().stringValue(); + teavm.setOptimizationLevel(switch (level) { + case "simple" -> TeaVMOptimizationLevel.SIMPLE; + case "full" -> TeaVMOptimizationLevel.FULL; + default -> TeaVMOptimizationLevel.ADVANCED; + }); new PlatformPlugin().install(teavm); new JCLPlugin().install(teavm); + new JSOPlugin().install(teavm); + addBootstrapPlugins(teavm); teavm.setEntryPoint(mainClass); target.setObfuscated(false); target.setDebugInfoLocation(WasmDebugInfoLocation.EMBEDDED); @@ -297,6 +453,34 @@ return teavm.getProblemProvider().getSevereProblems().isEmpty(); } + private static void addBootstrapPlugins(TeaVM teavm) { + var object = ValueType.object("java.lang.Object"); + var cls = ValueType.object("java.lang.Class"); + var integer = ValueType.INTEGER; + teavm.add(new MethodReference("java.lang.Object", "clone", object), new ObjectDependencyPlugin()); + var classGenerator = new ClassGenerator(); + teavm.add(new MethodReference("java.lang.Class", "getSuperclass", cls), classGenerator); + teavm.add(new MethodReference("java.lang.Class", "getInterfaces", ValueType.arrayOf(cls)), classGenerator); + teavm.add(new MethodReference("java.lang.Class", "getComponentType", cls), classGenerator); + teavm.add(new MethodReference("java.lang.Class", "getEnumConstants", ValueType.arrayOf(object)), + classGenerator); + teavm.add(new MethodReference("java.lang.Class", "getEnumConstantsFast", ValueType.arrayOf(object)), + classGenerator); + teavm.add(new MethodReference("java.lang.Class", "newInstance", object), classGenerator); + teavm.add(new MethodReference("java.lang.Class", "getDeclaredClasses", ValueType.arrayOf(cls)), + classGenerator); + var array = new ArrayDependencyPlugin(); + teavm.add(new MethodReference("java.lang.reflect.Array", "getLength", object, integer), array); + teavm.add(new MethodReference("java.lang.reflect.Array", "newInstance", cls, integer, object), array); + teavm.add(new MethodReference("java.lang.reflect.Array", "get", object, integer, object), array); + teavm.add(new MethodReference("java.lang.reflect.Array", "set", object, integer, object, ValueType.VOID), + array); + teavm.add(new MethodReference("java.lang.System", "doArrayCopy", object, integer, object, integer, integer, + ValueType.VOID), new SystemDependencyPlugin()); + teavm.add(new MethodReference("java.lang.String", "intern", ValueType.object("java.lang.String")), + new StringNativeDependency()); + } + @JSExport public ListenerRegistration onDiagnostic(CompilerDiagnosticListener diagnosticListener) { var reg = new DiagnosticListenerRegistration(diagnosticListeners, diagnosticListener); @@ -309,7 +493,8 @@ return; } var context = new Context(); - context.put(DiagnosticListener.class, new DiagnosticListenerImpl(diagnosticListeners)); + var listener = new DiagnosticListenerImpl(diagnosticListeners); + context.put(DiagnosticListener.class, listener); var fileManager = new FileManagerImpl(sourceFiles, classFiles, sdkFiles, outputFiles); context.put(JavaFileManager.class, fileManager); compiler = new SimpleJavaCompiler(context); --- a/compiler/src/main/java/org/teavm/javac/Worker.java +++ b/compiler/src/main/java/org/teavm/javac/Worker.java @@ -227,6 +227,11 @@ public JSBoolean getObfuscated() { return JSBoolean.valueOf(true); } + + @Override + public JSString getOptimization() { + return JSString.valueOf("advanced"); + } }; var reg = compiler.onDiagnostic(diagnostic -> handleTeaVMDiagnostic((TeaVMDiagnostic) diagnostic, requestId)); var result = compiler.generateWebAssembly(options); --- a/compiler/src/main/java/org/teavm/javac/WebAssemblyCompilationOptions.java +++ b/compiler/src/main/java/org/teavm/javac/WebAssemblyCompilationOptions.java @@ -33,4 +33,7 @@ @JSProperty JSBoolean getObfuscated(); + + @JSProperty + JSString getOptimization(); } --- a/compiler/src/main/java/org/teavm/javac/ArchiveBuilder.java +++ b/compiler/src/main/java/org/teavm/javac/ArchiveBuilder.java @@ -17,6 +17,7 @@ package org.teavm.javac; import java.io.Closeable; +import java.io.File; import java.io.DataOutputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -55,7 +56,7 @@ var output = new ArchiveBuilder(Files.newOutputStream(outPath))) { input.forEach(file -> { if (Files.isRegularFile(file)) { - var relName = root.relativize(file).toString(); + var relName = root.relativize(file).toString().replace(File.separatorChar, '/'); try { output.append(relName, Files.readAllBytes(file)); } catch (IOException e) { --- a/compiler/src/main/java/org/teavm/javac/DiagnosticListenerImpl.java +++ b/compiler/src/main/java/org/teavm/javac/DiagnosticListenerImpl.java @@ -13,16 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.teavm.javac; +import com.sun.tools.javac.api.DiagnosticFormatter; +import com.sun.tools.javac.util.JCDiagnostic; import java.util.List; +import java.util.Locale; import javax.tools.Diagnostic; import javax.tools.DiagnosticListener; import javax.tools.JavaFileObject; class DiagnosticListenerImpl implements DiagnosticListener { private final List regs; + DiagnosticFormatter formatter; DiagnosticListenerImpl(List regs) { this.regs = regs; @@ -33,7 +36,15 @@ if (regs.isEmpty()) { return; } - var wrapper = new JavaDiagnostic(diagnostic); + String message = null; + if (formatter != null && diagnostic instanceof JCDiagnostic jc) { + try { + message = formatter.formatMessage(jc, Locale.ENGLISH); + } catch (RuntimeException e) { + message = null; + } + } + var wrapper = new JavaDiagnostic(diagnostic, message); for (var reg : regs) { reg.listener.onDiagnostic(wrapper); } --- a/compiler/src/main/java/org/teavm/javac/JavaDiagnostic.java +++ b/compiler/src/main/java/org/teavm/javac/JavaDiagnostic.java @@ -24,9 +24,15 @@ public class JavaDiagnostic extends BaseDiagnostic { private final Diagnostic innerDiagnostic; + private final String message; public JavaDiagnostic(Diagnostic innerDiagnostic) { + this(innerDiagnostic, null); + } + + public JavaDiagnostic(Diagnostic innerDiagnostic, String message) { this.innerDiagnostic = innerDiagnostic; + this.message = message; } @Override @@ -51,7 +57,7 @@ @JSExport @JSProperty public String getMessage() { - return innerDiagnostic.getMessage(Locale.ENGLISH); + return message != null ? message : innerDiagnostic.getMessage(Locale.ENGLISH); } @Override --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ teavm = "0.16.0" asm = "9.8" junit-jupiter = "5.11.4" +checkstyle = "10.21.1" [libraries]