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 index 71e252f39e..9d2b35ea39 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/BytecodeInstructionIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/BytecodeInstructionIntegrationTest.java @@ -1,12 +1,30 @@ package com.codename1.tools.translator; +import com.codename1.tools.translator.bytecodes.ArrayLengthExpression; +import com.codename1.tools.translator.bytecodes.ArrayLoadExpression; +import com.codename1.tools.translator.bytecodes.AssignableExpression; +import com.codename1.tools.translator.bytecodes.Instruction; +import com.codename1.tools.translator.bytecodes.MultiArray; import org.junit.jupiter.api.Test; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Opcodes; import javax.tools.JavaCompiler; import javax.tools.ToolProvider; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FilenameFilter; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.*; @@ -90,7 +108,197 @@ void translatesOptimizedBytecodeToLLVMExecutable() throws Exception { Path executable = buildDir.resolve("CustomBytecodeApp"); String output = CleanTargetIntegrationTest.runCommand(Arrays.asList(executable.toString()), buildDir); - assertTrue(output.contains("RESULT=54"), "Compiled program should print the expected arithmetic result"); + assertTrue(output.contains("RESULT=293"), "Compiled program should print the expected arithmetic result"); + } + + private Set snapshotArrayTypes() throws Exception { + Field arrayTypesField = ByteCodeClass.class.getDeclaredField("arrayTypes"); + arrayTypesField.setAccessible(true); + return new TreeSet<>((Set) arrayTypesField.get(null)); + } + + private void restoreArrayTypes(Set snapshot) throws Exception { + Field arrayTypesField = ByteCodeClass.class.getDeclaredField("arrayTypes"); + arrayTypesField.setAccessible(true); + arrayTypesField.set(null, new TreeSet<>(snapshot)); + } + + private static class StubAssignableExpression extends Instruction implements AssignableExpression { + private final String expression; + int dependencyCalls; + + private StubAssignableExpression(int opcode, String expression) { + super(opcode); + this.expression = expression; + } + + @Override + public void addDependencies(List dependencyList) { + dependencyCalls++; + dependencyList.add(expression); + } + + @Override + public void appendInstruction(StringBuilder b) { + b.append(expression); + } + + @Override + public boolean assignTo(String varName, StringBuilder sb) { + if (varName != null) { + sb.append(varName).append(" = ").append(expression).append(";\n"); + } else { + sb.append(expression); + } + return true; + } + } + + @Test + void annotationVisitorWrapperDelegatesAndHandlesNullVisitor() { + Parser parser = new Parser(); + + Parser.AnnotationVisitorWrapper wrapperWithNull = parser.new AnnotationVisitorWrapper(null); + assertNull(wrapperWithNull.visitArray("values")); + assertNull(wrapperWithNull.visitAnnotation("name", "LExample;")); + assertDoesNotThrow(() -> wrapperWithNull.visit("flag", true)); + assertDoesNotThrow(() -> wrapperWithNull.visitEnum("choice", "LExample;", "VALUE")); + + AtomicBoolean delegated = new AtomicBoolean(false); + AnnotationVisitor delegate = new AnnotationVisitor(Opcodes.ASM5) { + @Override + public AnnotationVisitor visitArray(String name) { + delegated.set(true); + return this; + } + }; + + Parser.AnnotationVisitorWrapper wrapperWithDelegate = parser.new AnnotationVisitorWrapper(delegate); + AnnotationVisitor result = wrapperWithDelegate.visitArray("values"); + assertSame(delegate, result); + assertTrue(delegated.get(), "AnnotationVisitorWrapper should forward to the underlying visitor"); + } + + @Test + void byteCodeTranslatorFilenameFilterMatchesExpectedFiles() throws Exception { + Class filterClass = Class.forName("com.codename1.tools.translator.ByteCodeTranslator$3"); + Constructor ctor = filterClass.getDeclaredConstructor(); + ctor.setAccessible(true); + + FilenameFilter filter = (FilenameFilter) ctor.newInstance(); + File directory = Files.createTempDirectory("bytecode-filter").toFile(); + + assertTrue(filter.accept(directory, "assets.bundle")); + assertTrue(filter.accept(directory, "model.xcdatamodeld")); + assertTrue(filter.accept(directory, "VisibleSource.m")); + assertFalse(filter.accept(directory, ".hidden")); + assertFalse(filter.accept(directory, "Images.xcassets")); + } + + @Test + void concatenatingFileOutputStreamWritesShardedOutputs() throws Exception { + Path outputDir = Files.createTempDirectory("concatenating-output"); + ByteCodeTranslator.OutputType original = ByteCodeTranslator.output; + ByteCodeTranslator.output = ByteCodeTranslator.OutputType.OUTPUT_TYPE_CLEAN; + try { + ConcatenatingFileOutputStream stream = new ConcatenatingFileOutputStream(outputDir.toFile()); + stream.beginNextFile("first"); + stream.write("abc".getBytes(StandardCharsets.UTF_8)); + stream.close(); + + stream.beginNextFile("second"); + stream.write("123".getBytes(StandardCharsets.UTF_8)); + stream.close(); + + Field destField = ConcatenatingFileOutputStream.class.getDeclaredField("dest"); + destField.setAccessible(true); + ByteArrayOutputStream[] buffers = (ByteArrayOutputStream[]) destField.get(stream); + for (int i = 0; i < buffers.length; i++) { + if (buffers[i] == null) { + buffers[i] = new ByteArrayOutputStream(); + } + } + destField.set(stream, buffers); + + stream.realClose(); + + Path first = outputDir.resolve("concatenated_" + Math.abs("first".hashCode() % ConcatenatingFileOutputStream.MODULO) + ".c"); + Path second = outputDir.resolve("concatenated_" + Math.abs("second".hashCode() % ConcatenatingFileOutputStream.MODULO) + ".c"); + + assertTrue(Files.exists(first)); + assertEquals("abc\n", new String(Files.readAllBytes(first), StandardCharsets.UTF_8)); + + assertTrue(Files.exists(second)); + assertEquals("123\n", new String(Files.readAllBytes(second), StandardCharsets.UTF_8)); + } finally { + ByteCodeTranslator.output = original; + } + } + + @Test + void multiArrayAddsDependenciesAndRegistersArrayTypes() throws Exception { + List dependencies = new ArrayList<>(); + MultiArray multiArray = new MultiArray("[[Ljava/lang/String;", 2); + + Set snapshot = snapshotArrayTypes(); + try { + multiArray.addDependencies(dependencies); + + assertTrue(dependencies.contains("java_lang_String")); + assertTrue(snapshotArrayTypes().contains("2_java_lang_String")); + } finally { + restoreArrayTypes(snapshot); + } + } + + @Test + void arrayLengthExpressionReducesAndAssigns() throws Exception { + List instructions = new ArrayList<>(); + StubAssignableExpression arrayRef = new StubAssignableExpression(Opcodes.ALOAD, "myArray"); + Instruction arrayLength = new Instruction(Opcodes.ARRAYLENGTH) { }; + instructions.add(arrayRef); + instructions.add(arrayLength); + + int reducedIndex = ArrayLengthExpression.tryReduce(instructions, 1); + assertEquals(0, reducedIndex); + assertEquals(1, instructions.size()); + ArrayLengthExpression reduced = (ArrayLengthExpression) instructions.get(0); + + StringBuilder assignment = new StringBuilder(); + assertTrue(reduced.assignTo("len", assignment)); + assertEquals("len = CN1_ARRAY_LENGTH(myArray);\n", assignment.toString()); + + List deps = new ArrayList<>(); + reduced.addDependencies(deps); + assertEquals(1, arrayRef.dependencyCalls); + assertEquals("myArray", deps.get(0)); + } + + @Test + void arrayLoadExpressionReducesAndAssigns() { + List instructions = new ArrayList<>(); + StubAssignableExpression arrayRef = new StubAssignableExpression(Opcodes.ALOAD, "items"); + StubAssignableExpression index = new StubAssignableExpression(Opcodes.ILOAD, "index"); + Instruction loadInstr = new Instruction(Opcodes.IALOAD) { }; + instructions.add(arrayRef); + instructions.add(index); + instructions.add(loadInstr); + + int reducedIndex = ArrayLoadExpression.tryReduce(instructions, 2); + assertEquals(0, reducedIndex); + assertEquals(1, instructions.size()); + ArrayLoadExpression reduced = (ArrayLoadExpression) instructions.get(0); + + StringBuilder assignment = new StringBuilder(); + assertTrue(reduced.assignTo("value", assignment)); + assertEquals("value=CN1_ARRAY_ELEMENT_INT(items, index);\n", assignment.toString()); + + List deps = new ArrayList<>(); + reduced.addDependencies(deps); + assertEquals(1, arrayRef.dependencyCalls); + assertEquals(1, index.dependencyCalls); + assertTrue(deps.contains("items")); + assertTrue(deps.contains("index")); } private Path findGeneratedSource(Path srcRoot) throws Exception { @@ -104,7 +312,14 @@ private Path findGeneratedSource(Path srcRoot) throws Exception { private String appSource() { return "public class BytecodeInstructionApp {\n" + + " private static final int STATIC_INCREMENT = 3;\n" + " private static native void report(int value);\n" + + " private int instanceCounter = 7;\n" + + " private int baseField = 11;\n" + + " public BytecodeInstructionApp(int seed) {\n" + + " instanceCounter = seed;\n" + + " baseField = seed + 6;\n" + + " }\n" + " private static int optimizedComputation(int a, int b) {\n" + " int counter = a;\n" + " counter++;\n" + @@ -136,12 +351,73 @@ private String appSource() { " }\n" + " return result;\n" + " }\n" + + " private int loopArrays(int base) {\n" + + " int[] values = { base, base + 1, base + 2, STATIC_INCREMENT };\n" + + " int total = 0;\n" + + " for (int i = 0; i < values.length; i++) {\n" + + " total += values[i] * (i + 1);\n" + + " }\n" + + " return total + values.length;\n" + + " }\n" + + " private int multiArrayUsage(int factor) {\n" + + " int[][] grid = new int[2][3];\n" + + " int v = factor;\n" + + " for (int i = 0; i < grid.length; i++) {\n" + + " for (int j = 0; j < grid[i].length; j++) {\n" + + " grid[i][j] = v++;\n" + + " }\n" + + " }\n" + + " int total = 0;\n" + + " for (int[] row : grid) {\n" + + " for (int cell : row) {\n" + + " total += cell;\n" + + " }\n" + + " total += row.length;\n" + + " }\n" + + " String[][] labels = new String[][] { { \"a\", \"b\" }, { \"c\", \"d\" } };\n" + + " int labelLength = 0;\n" + + " for (int i = 0; i < labels.length; i++) {\n" + + " labelLength += labels[i].length;\n" + + " }\n" + + " return total + labelLength;\n" + + " }\n" + + " private int useFieldsAndMethods(int offset) {\n" + + " instanceCounter += offset;\n" + + " int[] mix = new int[] { offset, offset + baseField, instanceCounter };\n" + + " int altSum = 0;\n" + + " for (int i = 0; i < mix.length; i++) {\n" + + " altSum += mix[i] + i;\n" + + " }\n" + + " int nestedLoop = loopArrays(offset);\n" + + " int nestedMulti = multiArrayUsage(offset);\n" + + " report(instanceCounter);\n" + + " report(baseField);\n" + + " report(altSum);\n" + + " report(nestedLoop);\n" + + " report(nestedMulti);\n" + + " return nestedLoop + nestedMulti + instanceCounter + altSum + baseField;\n" + + " }\n" + " public static void main(String[] args) {\n" + + " BytecodeInstructionApp app = new BytecodeInstructionApp(4);\n" + " int first = optimizedComputation(1, 3);\n" + " int second = optimizedComputation(5, 2);\n" + " int switched = switchComputation(first) + switchComputation(second);\n" + " int synchronizedValue = synchronizedIncrement(second);\n" + - " report(first + second + switched + synchronizedValue);\n" + + " int arrays = app.loopArrays(2);\n" + + " int multi = app.multiArrayUsage(3);\n" + + " int arraysFive = app.loopArrays(5);\n" + + " int multiFive = app.multiArrayUsage(5);\n" + + " int fieldCalls = app.useFieldsAndMethods(5);\n" + + " report(first);\n" + + " report(second);\n" + + " report(switched);\n" + + " report(synchronizedValue);\n" + + " report(arrays);\n" + + " report(multi);\n" + + " report(arraysFive);\n" + + " report(multiFive);\n" + + " report(fieldCalls);\n" + + " report(first + second + switched + synchronizedValue + arrays + multi + fieldCalls);\n" + " }\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 090cd5e53e..7f705cb2bf 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 @@ -219,7 +219,58 @@ static void writeRuntimeStubs(Path srcRoot) throws IOException { " free(obj);\n" + "}\n" + "\n" + - "void initConstantPool() {}\n" + + "JAVA_OBJECT* constantPoolObjects = NULL;\n" + + "\n" + + "void initConstantPool() {\n" + + " if (constantPoolObjects == NULL) {\n" + + " constantPoolObjects = calloc(32, sizeof(JAVA_OBJECT));\n" + + " }\n" + + "}\n" + + "\n" + + "void arrayFinalizerFunction(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT array) {\n" + + " (void)threadStateData;\n" + + " free(array);\n" + + "}\n" + + "\n" + + "void gcMarkArrayObject(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT obj, JAVA_BOOLEAN force) {\n" + + " (void)threadStateData;\n" + + " (void)obj;\n" + + " (void)force;\n" + + "}\n" + + "\n" + + "void** initVtableForInterface() {\n" + + " static void* table[1];\n" + + " return (void**)table;\n" + + "}\n" + + "\n" + + "struct clazz class_array1__JAVA_INT = {0};\n" + + "struct clazz class_array2__JAVA_INT = {0};\n" + + "\n" + + "static JAVA_OBJECT allocArrayInternal(CODENAME_ONE_THREAD_STATE, int length, struct clazz* type, int primitiveSize, int dim) {\n" + + " struct JavaArrayPrototype* arr = (struct JavaArrayPrototype*)calloc(1, sizeof(struct JavaArrayPrototype));\n" + + " arr->__codenameOneParentClsReference = type;\n" + + " arr->length = length;\n" + + " arr->dimensions = dim;\n" + + " arr->primitiveSize = primitiveSize;\n" + + " if (length > 0) {\n" + + " int elementSize = primitiveSize > 0 ? primitiveSize : sizeof(JAVA_OBJECT);\n" + + " arr->data = calloc((size_t)length, (size_t)elementSize);\n" + + " }\n" + + " return (JAVA_OBJECT)arr;\n" + + "}\n" + + "\n" + + "JAVA_OBJECT allocArray(CODENAME_ONE_THREAD_STATE, int length, struct clazz* type, int primitiveSize, int dim) {\n" + + " return allocArrayInternal(threadStateData, length, type, primitiveSize, dim);\n" + + "}\n" + + "\n" + + "JAVA_OBJECT alloc2DArray(CODENAME_ONE_THREAD_STATE, int length1, int length2, struct clazz* parentType, struct clazz* childType, int primitiveSize) {\n" + + " struct JavaArrayPrototype* outer = (struct JavaArrayPrototype*)allocArrayInternal(threadStateData, length1, parentType, sizeof(JAVA_OBJECT), 2);\n" + + " JAVA_OBJECT* rows = (JAVA_OBJECT*)outer->data;\n" + + " for (int i = 0; i < length1; i++) {\n" + + " rows[i] = allocArrayInternal(threadStateData, length2, childType, primitiveSize, 1);\n" + + " }\n" + + " return (JAVA_OBJECT)outer;\n" + + "}\n" + "\n" + "void initMethodStack(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT __cn1ThisObject, int stackSize, int localsStackSize, int classNameId, int methodNameId) {\n" + " (void)__cn1ThisObject;\n" +