diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/BytecodeInstructionIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/BytecodeInstructionIntegrationTest.java new file mode 100644 index 0000000000..2190b6281c --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/BytecodeInstructionIntegrationTest.java @@ -0,0 +1,134 @@ +package com.codename1.tools.translator; +import org.junit.jupiter.api.Test; + +import javax.tools.JavaCompiler; +import javax.tools.ToolProvider; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +class BytecodeInstructionIntegrationTest { + + @Test + void translatesOptimizedBytecodeToLLVMExecutable() throws Exception { + Parser.cleanup(); + + Path sourceDir = Files.createTempDirectory("bytecode-integration-sources"); + Path classesDir = Files.createTempDirectory("bytecode-integration-classes"); + + Files.createDirectories(sourceDir.resolve("java/lang")); + Files.write(sourceDir.resolve("BytecodeInstructionApp.java"), appSource().getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("java/lang/Object.java"), CleanTargetIntegrationTest.javaLangObjectSource().getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("java/lang/String.java"), CleanTargetIntegrationTest.javaLangStringSource().getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("java/lang/Class.java"), CleanTargetIntegrationTest.javaLangClassSource().getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("java/lang/Throwable.java"), CleanTargetIntegrationTest.javaLangThrowableSource().getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("java/lang/Exception.java"), CleanTargetIntegrationTest.javaLangExceptionSource().getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("java/lang/RuntimeException.java"), CleanTargetIntegrationTest.javaLangRuntimeExceptionSource().getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("java/lang/NullPointerException.java"), CleanTargetIntegrationTest.javaLangNullPointerExceptionSource().getBytes(StandardCharsets.UTF_8)); + + Path nativeReport = sourceDir.resolve("native_report.c"); + Files.write(nativeReport, nativeReportSource().getBytes(StandardCharsets.UTF_8)); + + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertNotNull(compiler, "A JDK is required to compile test sources"); + + int compileResult = compiler.run( + null, + null, + null, + "-d", classesDir.toString(), + sourceDir.resolve("BytecodeInstructionApp.java").toString(), + sourceDir.resolve("java/lang/Object.java").toString(), + sourceDir.resolve("java/lang/String.java").toString(), + sourceDir.resolve("java/lang/Class.java").toString(), + sourceDir.resolve("java/lang/Throwable.java").toString(), + sourceDir.resolve("java/lang/Exception.java").toString(), + sourceDir.resolve("java/lang/RuntimeException.java").toString(), + sourceDir.resolve("java/lang/NullPointerException.java").toString() + ); + assertEquals(0, compileResult, "BytecodeInstructionApp should compile"); + + Files.copy(nativeReport, classesDir.resolve("native_report.c")); + + Path outputDir = Files.createTempDirectory("bytecode-integration-output"); + CleanTargetIntegrationTest.runTranslator(classesDir, outputDir, "CustomBytecodeApp"); + + Path distDir = outputDir.resolve("dist"); + Path cmakeLists = distDir.resolve("CMakeLists.txt"); + assertTrue(Files.exists(cmakeLists), "Translator should emit a CMake project for the optimized sample"); + + Path srcRoot = distDir.resolve("CustomBytecodeApp-src"); + CleanTargetIntegrationTest.patchCn1Globals(srcRoot); + CleanTargetIntegrationTest.writeRuntimeStubs(srcRoot); + + Path generatedSource = findGeneratedSource(srcRoot); + String generatedCode = new String(Files.readAllBytes(generatedSource), StandardCharsets.UTF_8); + assertTrue(generatedCode.contains("CustomJump */"), "Optimized comparisons should emit CustomJump code"); + assertTrue(generatedCode.contains("BC_IINC"), "Increment operations should translate to BC_IINC macro"); + assertTrue(generatedCode.contains("VarOp.assignFrom"), "Optimized stores should rely on CustomIntruction output"); + + CleanTargetIntegrationTest.replaceLibraryWithExecutableTarget(cmakeLists, srcRoot.getFileName().toString()); + + Path buildDir = distDir.resolve("build"); + Files.createDirectories(buildDir); + + CleanTargetIntegrationTest.runCommand(Arrays.asList( + "cmake", + "-S", distDir.toString(), + "-B", buildDir.toString(), + "-DCMAKE_C_COMPILER=clang", + "-DCMAKE_OBJC_COMPILER=clang" + ), distDir); + + CleanTargetIntegrationTest.runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); + + Path executable = buildDir.resolve("CustomBytecodeApp"); + String output = CleanTargetIntegrationTest.runCommand(Arrays.asList(executable.toString()), buildDir); + assertTrue(output.contains("RESULT=14"), "Compiled program should print the expected arithmetic result"); + } + + private Path findGeneratedSource(Path srcRoot) throws Exception { + try (Stream paths = Files.walk(srcRoot)) { + return paths + .filter(p -> p.getFileName().toString().startsWith("BytecodeInstructionApp") && p.getFileName().toString().endsWith(".c")) + .findFirst() + .orElseThrow(() -> new AssertionError("Translated source for BytecodeInstructionApp not found")); + } + } + + private String appSource() { + return "public class BytecodeInstructionApp {\n" + + " private static native void report(int value);\n" + + " private static int optimizedComputation(int a, int b) {\n" + + " int counter = a;\n" + + " counter++;\n" + + " int branchBase = counter;\n" + + " int min;\n" + + " if (branchBase < b) {\n" + + " min = branchBase;\n" + + " } else {\n" + + " min = b + 2;\n" + + " }\n" + + " int result = min + counter;\n" + + " return result;\n" + + " }\n" + + " public static void main(String[] args) {\n" + + " int first = optimizedComputation(1, 3);\n" + + " int second = optimizedComputation(5, 2);\n" + + " report(first + second);\n" + + " }\n" + + "}\n"; + } + + private String nativeReportSource() { + return "#include \"cn1_globals.h\"\n" + + "#include \n" + + "void BytecodeInstructionApp_report___int(CODENAME_ONE_THREAD_STATE, JAVA_INT value) {\n" + + " printf(\"RESULT=%d\\n\", value);\n" + + "}\n"; + } +} diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java index 0818698c69..80c791ed0e 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java @@ -94,7 +94,7 @@ void generatesRunnableHelloWorldUsingCleanTarget() throws Exception { "Compiled program should print hello message, actual output was:\n" + output); } - private void runTranslator(Path classesDir, Path outputDir, String appName) throws Exception { + static void runTranslator(Path classesDir, Path outputDir, String appName) throws Exception { Path translatorResources = Paths.get("..", "ByteCodeTranslator", "src").normalize().toAbsolutePath(); URLClassLoader systemLoader = (URLClassLoader) ClassLoader.getSystemClassLoader(); URL[] systemUrls = systemLoader.getURLs(); @@ -138,7 +138,7 @@ private void runTranslator(Path classesDir, Path outputDir, String appName) thro } } - private void replaceLibraryWithExecutableTarget(Path cmakeLists, String sourceDirName) throws IOException { + static void replaceLibraryWithExecutableTarget(Path cmakeLists, String sourceDirName) throws IOException { String content = new String(Files.readAllBytes(cmakeLists), StandardCharsets.UTF_8); String globWithObjc = String.format("file(GLOB TRANSLATOR_SOURCES \"%s/*.c\" \"%s/*.m\")", sourceDirName, sourceDirName); String globCOnly = String.format("file(GLOB TRANSLATOR_SOURCES \"%s/*.c\")", sourceDirName); @@ -150,7 +150,7 @@ private void replaceLibraryWithExecutableTarget(Path cmakeLists, String sourceDi Files.write(cmakeLists, replacement.getBytes(StandardCharsets.UTF_8)); } - private String runCommand(List command, Path workingDir) throws Exception { + static String runCommand(List command, Path workingDir) throws Exception { ProcessBuilder builder = new ProcessBuilder(command); builder.directory(workingDir.toFile()); builder.redirectErrorStream(true); @@ -164,7 +164,7 @@ private String runCommand(List command, Path workingDir) throws Exceptio return output; } - private void patchCn1Globals(Path srcRoot) throws IOException { + static void patchCn1Globals(Path srcRoot) throws IOException { Path cn1Globals = srcRoot.resolve("cn1_globals.h"); String content = new String(Files.readAllBytes(cn1Globals), StandardCharsets.UTF_8); if (!content.contains("@class NSString;")) { @@ -173,7 +173,7 @@ private void patchCn1Globals(Path srcRoot) throws IOException { } } - private void writeRuntimeStubs(Path srcRoot) throws IOException { + static void writeRuntimeStubs(Path srcRoot) throws IOException { Path stubs = srcRoot.resolve("runtime_stubs.c"); if (Files.exists(stubs)) { return; @@ -243,7 +243,7 @@ private void writeRuntimeStubs(Path srcRoot) throws IOException { Files.write(stubs, content.getBytes(StandardCharsets.UTF_8)); } - private String helloWorldSource() { + static String helloWorldSource() { return "public class HelloWorld {\n" + " private static native void nativeHello();\n" + " public static void main(String[] args) {\n" + @@ -252,49 +252,49 @@ private String helloWorldSource() { "}\n"; } - private String javaLangObjectSource() { + static String javaLangObjectSource() { return "package java.lang;\n" + "public class Object {\n" + "}\n"; } - private String javaLangStringSource() { + static String javaLangStringSource() { return "package java.lang;\n" + "public class String extends Object {\n" + "}\n"; } - private String javaLangClassSource() { + static String javaLangClassSource() { return "package java.lang;\n" + "public final class Class extends Object {\n" + "}\n"; } - private String javaLangThrowableSource() { + static String javaLangThrowableSource() { return "package java.lang;\n" + "public class Throwable extends Object {\n" + "}\n"; } - private String javaLangExceptionSource() { + static String javaLangExceptionSource() { return "package java.lang;\n" + "public class Exception extends Throwable {\n" + "}\n"; } - private String javaLangRuntimeExceptionSource() { + static String javaLangRuntimeExceptionSource() { return "package java.lang;\n" + "public class RuntimeException extends Exception {\n" + "}\n"; } - private String javaLangNullPointerExceptionSource() { + static String javaLangNullPointerExceptionSource() { return "package java.lang;\n" + "public class NullPointerException extends RuntimeException {\n" + "}\n"; } - private String nativeHelloSource() { + static String nativeHelloSource() { return "#include \"cn1_globals.h\"\n" + "#include \n" + "void HelloWorld_nativeHello__(CODENAME_ONE_THREAD_STATE) {\n" +