diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java index e43eb13429..46fbebdad6 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java @@ -93,6 +93,13 @@ public void setIsAnnotation(boolean isAnnotation) { private boolean finalClass; private boolean isEnum; private static Set writableFields = new HashSet(); + + static void cleanup() { + arrayTypes.clear(); + writableFields.clear(); + mainClass = null; + saveUnitTests = false; + } /** * @@ -170,6 +177,10 @@ public void addField(ByteCodeField m) { public String generateCSharpCode() { return ""; } + + public String generateJavascriptCode(List allClasses) { + return JavascriptMethodGenerator.generateClassJavascript(this, allClasses); + } public void addWritableField(String field) { writableFields.add(field); diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java index 45e8b1d812..355a5dc9d1 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java @@ -64,6 +64,13 @@ public String extension() { return "c"; } + }, + OUTPUT_TYPE_JAVASCRIPT { + @Override + public String extension() { + return "js"; + } + }; public abstract String extension(); @@ -144,7 +151,7 @@ public static void main(String[] args) throws Exception { } if(args.length != 9) { - System.out.println("We accept 9 arguments output type (ios, csharp, clean), input directory, output directory, app name, package name, app dispaly name, version, type (ios/iphone/ipad) and additional frameworks"); + System.out.println("We accept 9 arguments output type (ios, csharp, clean, javascript), input directory, output directory, app name, package name, app dispaly name, version, type (ios/iphone/ipad) and additional frameworks"); System.exit(1); return; } @@ -159,10 +166,14 @@ public static void main(String[] args) throws Exception { System.out.println("Generating Unit Tests"); ByteCodeClass.setSaveUnitTests(true); } - if(args[0].equalsIgnoreCase("csharp")) { + if(args[0].equalsIgnoreCase("ios")) { + output = OutputType.OUTPUT_TYPE_IOS; + } else if(args[0].equalsIgnoreCase("csharp")) { output = OutputType.OUTPUT_TYPE_CSHARP; } else if(args[0].equalsIgnoreCase("clean")) { output = OutputType.OUTPUT_TYPE_CLEAN; + } else if(args[0].equalsIgnoreCase("javascript")) { + output = OutputType.OUTPUT_TYPE_JAVASCRIPT; } String[] sourceDirectories = args[1].split(";"); File[] sources = new File[sourceDirectories.length]; @@ -189,6 +200,9 @@ public static void main(String[] args) throws Exception { case OUTPUT_TYPE_CLEAN: handleCleanOutput(b, sources, dest, appName); break; + case OUTPUT_TYPE_JAVASCRIPT: + handleJavascriptOutput(b, sources, dest, appName); + break; default: handleDefaultOutput(b, sources, dest); } @@ -250,6 +264,18 @@ private static void handleCleanOutput(ByteCodeTranslator b, File[] sources, File writeCmakeProject(root, srcRoot, appName); } + private static void handleJavascriptOutput(ByteCodeTranslator b, File[] sources, File dest, String appName) throws Exception { + File root = new File(dest, "dist"); + root.mkdirs(); + if(verbose) { + System.out.println("Root is: " + root.getAbsolutePath()); + } + File srcRoot = new File(root, appName + "-js"); + srcRoot.mkdirs(); + b.execute(sources, srcRoot); + Parser.writeOutput(srcRoot); + } + private static void handleIosOutput(ByteCodeTranslator b, File[] sources, File dest, String appName, String appPackageName, String appDisplayName, String appVersion, String appType, String addFrameworks) throws Exception { File root = new File(dest, "dist"); root.mkdirs(); diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java index 991ed5d930..6ccee6d0e4 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java @@ -747,6 +747,44 @@ public void addToConstantPool() { public boolean isSynchronizedMethod() { return synchronizedMethod; } + + public List getInstructions() { + return instructions; + } + + public List getArguments() { + return arguments; + } + + public ByteCodeMethodArg getReturnType() { + return returnType; + } + + public int getMaxStack() { + return maxStack; + } + + public int getMaxLocals() { + return maxLocals; + } + + public boolean isConstructor() { + return constructor; + } + + public String getMethodIdentifier() { + StringBuilder b = new StringBuilder(); + b.append(clsName).append("_"); + if(methodName.equals("")) { + b.append("__INIT__"); + } else if(methodName.equals("")) { + b.append("__CLINIT__"); + } else { + b.append(getCMethodName()); + } + appendMethodSignatureSuffixFromDesc(desc, b, new ArrayList()); + return b.toString(); + } private boolean hasLocalVariableWithIndex(char qualifier, int index) { for (LocalVariable lv : localVariables) { diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java new file mode 100644 index 0000000000..cf4f9f3547 --- /dev/null +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptBundleWriter.java @@ -0,0 +1,96 @@ +package com.codename1.tools.translator; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +final class JavascriptBundleWriter { + private static final String RESOURCE_ROOT = "/javascript/"; + + private JavascriptBundleWriter() { + } + + static void write(File outputDirectory, List classes) throws IOException { + writeRuntime(outputDirectory); + writeTranslatedClasses(outputDirectory, classes); + writeWorker(outputDirectory); + writeIndex(outputDirectory); + } + + private static void writeRuntime(File outputDirectory) throws IOException { + writeResource(outputDirectory, "parparvm_runtime.js", "parparvm_runtime.js"); + } + + private static void writeTranslatedClasses(File outputDirectory, List classes) throws IOException { + StringBuilder out = new StringBuilder(); + for (ByteCodeClass cls : classes) { + out.append(cls.generateJavascriptCode(classes)).append('\n'); + } + ByteCodeClass mainClass = ByteCodeClass.getMainClass(); + if (mainClass != null) { + out.append("jvm.setMain(\"").append(mainClass.getClsName()).append("\", \"") + .append(JavascriptNameUtil.methodIdentifier(mainClass.getClsName(), "main", "([Ljava/lang/String;)V")) + .append("\");\n"); + } + Files.write(new File(outputDirectory, "translated_app.js").toPath(), + out.toString().getBytes(StandardCharsets.UTF_8)); + } + + private static void writeWorker(File outputDirectory) throws IOException { + List nativeScripts = new ArrayList(); + File[] files = outputDirectory.listFiles(); + if (files != null) { + for (File file : files) { + String name = file.getName(); + if (!name.endsWith(".js")) { + continue; + } + if ("parparvm_runtime.js".equals(name) || "translated_app.js".equals(name) || "worker.js".equals(name)) { + continue; + } + nativeScripts.add(name); + } + } + + StringBuilder imports = new StringBuilder(); + imports.append("importScripts('parparvm_runtime.js');\n"); + for (String script : nativeScripts) { + imports.append("importScripts('").append(script).append("');\n"); + } + imports.append("importScripts('translated_app.js');\n"); + + String worker = loadResource("worker.js").replace("/*__IMPORTS__*/", imports.toString().trim()); + Files.write(new File(outputDirectory, "worker.js").toPath(), worker.getBytes(StandardCharsets.UTF_8)); + } + + private static void writeIndex(File outputDirectory) throws IOException { + writeResource(outputDirectory, "index.html", "index.html"); + } + + private static void writeResource(File outputDirectory, String targetName, String resourceName) throws IOException { + Files.write(new File(outputDirectory, targetName).toPath(), + loadResource(resourceName).getBytes(StandardCharsets.UTF_8)); + } + + private static String loadResource(String resourceName) throws IOException { + InputStream input = JavascriptBundleWriter.class.getResourceAsStream(RESOURCE_ROOT + resourceName); + if (input == null) { + throw new IOException("Missing javascript backend resource " + resourceName); + } + try { + byte[] data = new byte[8192]; + StringBuilder out = new StringBuilder(); + int len; + while ((len = input.read(data)) > -1) { + out.append(new String(data, 0, len, StandardCharsets.UTF_8)); + } + return out.toString(); + } finally { + input.close(); + } + } +} diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java new file mode 100644 index 0000000000..535be59c85 --- /dev/null +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java @@ -0,0 +1,1653 @@ +package com.codename1.tools.translator; + +import com.codename1.tools.translator.bytecodes.BasicInstruction; +import com.codename1.tools.translator.bytecodes.Field; +import com.codename1.tools.translator.bytecodes.IInc; +import com.codename1.tools.translator.bytecodes.Instruction; +import com.codename1.tools.translator.bytecodes.Invoke; +import com.codename1.tools.translator.bytecodes.Jump; +import com.codename1.tools.translator.bytecodes.LabelInstruction; +import com.codename1.tools.translator.bytecodes.Ldc; +import com.codename1.tools.translator.bytecodes.LineNumber; +import com.codename1.tools.translator.bytecodes.LocalVariable; +import com.codename1.tools.translator.bytecodes.MultiArray; +import com.codename1.tools.translator.bytecodes.SwitchInstruction; +import com.codename1.tools.translator.bytecodes.TryCatch; +import com.codename1.tools.translator.bytecodes.TypeInstruction; +import com.codename1.tools.translator.bytecodes.VarOp; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.objectweb.asm.Label; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +final class JavascriptMethodGenerator { + private JavascriptMethodGenerator() { + } + + static String generateClassJavascript(ByteCodeClass cls, List allClasses) { + StringBuilder out = new StringBuilder(); + out.append("// ").append(cls.getClsName()).append("\n"); + appendClassRegistration(out, cls, allClasses); + for (BytecodeMethod method : cls.getMethods()) { + if (method.isNative() || method.isAbstract() || method.isEliminated()) { + continue; + } + appendMethod(out, cls, method); + } + for (BytecodeMethod method : cls.getMethods()) { + if (!method.isNative() || method.isEliminated()) { + continue; + } + appendNativeStubIfNeeded(out, cls, method); + if (!method.isStatic() && !method.isConstructor()) { + String jsMethodName = jsMethodIdentifier(cls, method); + out.append("jvm.addVirtualMethod(\"").append(cls.getClsName()).append("\", \"") + .append(jsMethodName).append("\", ") + .append(jsMethodName).append(");\n"); + } + } + return out.toString(); + } + + private static void appendClassRegistration(StringBuilder out, ByteCodeClass cls, List allClasses) { + out.append("jvm.defineClass({\n"); + out.append(" name: \"").append(cls.getClsName()).append("\",\n"); + out.append(" baseClass: "); + if (cls.getBaseClass() == null) { + out.append("null"); + } else { + out.append("\"").append(JavascriptNameUtil.sanitizeClassName(cls.getBaseClass())).append("\""); + } + out.append(",\n"); + out.append(" interfaces: ["); + boolean first = true; + for (String iface : cls.getBaseInterfaces()) { + if (!first) { + out.append(", "); + } + first = false; + out.append("\"").append(JavascriptNameUtil.sanitizeClassName(iface)).append("\""); + } + out.append("],\n"); + out.append(" isInterface: ").append(cls.isIsInterface()).append(",\n"); + out.append(" isAbstract: ").append(cls.isIsAbstract()).append(",\n"); + appendAssignableTypes(out, cls, allClasses); + out.append(" instanceFields: ["); + first = true; + for (ByteCodeField field : cls.getFields()) { + if (field.isStaticField()) { + continue; + } + if (!first) { + out.append(", "); + } + first = false; + out.append("{ owner: \"").append(field.getClsName()).append("\", name: \"") + .append(field.getFieldName()).append("\", desc: \"") + .append(JavascriptNameUtil.escapeJs(field.getType() == null ? "" : field.getType())).append("\", prop: \"") + .append(JavascriptNameUtil.fieldProperty(field.getClsName(), field.getFieldName())).append("\" }"); + } + out.append("],\n"); + out.append(" staticFields: {"); + first = true; + for (ByteCodeField field : cls.getFields()) { + if (!field.isStaticField()) { + continue; + } + if (!first) { + out.append(", "); + } + first = false; + out.append("\"").append(field.getFieldName()).append("\": ") + .append(field.getValue() == null ? JavascriptNameUtil.defaultValue(field.getType()) : renderStaticConstant(field)); + } + out.append("},\n"); + out.append(" methods: {},\n"); + out.append(" classObject: null\n"); + out.append("});\n"); + } + + private static void appendAssignableTypes(StringBuilder out, ByteCodeClass cls, List allClasses) { + List assignableTypes = new java.util.ArrayList(); + collectAssignableTypes(cls, allClasses, assignableTypes); + out.append(" assignableTo: {"); + for (int i = 0; i < assignableTypes.size(); i++) { + if (i > 0) { + out.append(", "); + } + out.append("\"").append(assignableTypes.get(i)).append("\": true"); + } + out.append("},\n"); + } + + private static void collectAssignableTypes(ByteCodeClass cls, List allClasses, List out) { + addAssignableType(out, JavascriptNameUtil.runtimeTypeName(cls.getClsName())); + addAssignableType(out, "java_lang_Object"); + collectAssignableTypesFromBase(cls.getBaseClass(), allClasses, out); + for (String iface : cls.getBaseInterfaces()) { + collectAssignableTypesFromBase(iface, allClasses, out); + } + } + + private static void collectAssignableTypesFromBase(String className, List allClasses, List out) { + String normalized = JavascriptNameUtil.runtimeTypeName(className); + if (normalized == null || containsType(out, normalized)) { + return; + } + addAssignableType(out, normalized); + ByteCodeClass base = findClass(normalized, allClasses); + if (base == null) { + return; + } + collectAssignableTypesFromBase(base.getBaseClass(), allClasses, out); + for (String iface : base.getBaseInterfaces()) { + collectAssignableTypesFromBase(iface, allClasses, out); + } + } + + private static void addAssignableType(List out, String className) { + if (className != null && !containsType(out, className)) { + out.add(className); + } + } + + private static boolean containsType(List types, String className) { + for (int i = 0; i < types.size(); i++) { + if (className.equals(types.get(i))) { + return true; + } + } + return false; + } + + private static ByteCodeClass findClass(String className, List allClasses) { + for (int i = 0; i < allClasses.size(); i++) { + ByteCodeClass candidate = allClasses.get(i); + if (className.equals(candidate.getClsName())) { + return candidate; + } + } + return null; + } + + private static String renderStaticConstant(ByteCodeField field) { + Object value = field.getValue(); + if (value instanceof String) { + return "jvm.createStringLiteral(\"" + JavascriptNameUtil.escapeJs((String) value) + "\")"; + } + if (value instanceof Boolean) { + return ((Boolean) value).booleanValue() ? "1" : "0"; + } + if (value instanceof Number) { + return value.toString(); + } + return JavascriptNameUtil.defaultValue(field.getType()); + } + + private static void appendMethod(StringBuilder out, ByteCodeClass cls, BytecodeMethod method) { + List instructions = method.getInstructions(); + Map labelToIndex = buildLabelMap(instructions); + String jsMethodName = jsMethodIdentifier(cls, method); + out.append("function* ").append(jsMethodName).append("("); + boolean first = true; + if (!method.isStatic()) { + out.append("__cn1ThisObject"); + first = false; + } + List arguments = method.getArguments(); + for (int i = 0; i < arguments.size(); i++) { + if (!first) { + out.append(", "); + } + first = false; + out.append("__cn1Arg").append(i + 1); + } + out.append("){\n"); + if (method.isStatic() && !"__CLINIT__".equals(method.getMethodName())) { + out.append(" jvm.ensureClassInitialized(\"").append(cls.getClsName()).append("\");\n"); + } + if (appendStraightLineMethodBody(out, cls, method, instructions, jsMethodName)) { + return; + } + out.append(" const locals = new Array(").append(Math.max(1, method.getMaxLocals())).append(").fill(null);\n"); + out.append(" const stack = [];\n"); + out.append(" let pc = 0;\n"); + if (!method.isStatic()) { + out.append(" locals[0] = __cn1ThisObject;\n"); + } + int localIndex = method.isStatic() ? 0 : 1; + for (int i = 0; i < arguments.size(); i++) { + out.append(" locals[").append(localIndex).append("] = __cn1Arg").append(i + 1).append(";\n"); + localIndex++; + if (arguments.get(i).isDoubleOrLong()) { + localIndex++; + } + } + appendTryCatchTable(out, instructions, labelToIndex); + if (method.isSynchronizedMethod()) { + out.append(" const __cn1Monitor = ").append(method.isStatic() ? "jvm.getClassObject(\"" + cls.getClsName() + "\")" : "__cn1ThisObject").append(";\n"); + out.append(" jvm.monitorEnter(jvm.currentThread, __cn1Monitor);\n"); + out.append(" try {\n"); + } + out.append(" while (true) {\n"); + out.append(" try {\n"); + out.append(" switch (pc) {\n"); + for (int i = 0; i < instructions.size(); i++) { + Instruction instruction = instructions.get(i); + out.append(" case ").append(i).append(": {\n"); + appendInstruction(out, method, instructions, labelToIndex, instruction, i); + out.append(" }\n"); + } + out.append(" default:\n"); + out.append(" return null;\n"); + out.append(" }\n"); + out.append(" } catch (__cn1Error) {\n"); + out.append(" const __handler = jvm.findExceptionHandler(__cn1TryCatch, pc, __cn1Error);\n"); + out.append(" if (!__handler) {\n"); + out.append(" throw __cn1Error;\n"); + out.append(" }\n"); + out.append(" stack.length = 0;\n"); + out.append(" stack.push(__cn1Error);\n"); + out.append(" pc = __handler.handler;\n"); + out.append(" }\n"); + out.append(" }\n"); + if (method.isSynchronizedMethod()) { + out.append(" } finally {\n"); + out.append(" jvm.monitorExit(jvm.currentThread, __cn1Monitor);\n"); + out.append(" }\n"); + } + out.append("}\n"); + if ("__CLINIT__".equals(method.getMethodName())) { + out.append("jvm.classes[\"").append(cls.getClsName()).append("\"].clinit = ").append(jsMethodName).append(";\n"); + } + if (!method.isStatic() && !method.isConstructor()) { + out.append("jvm.addVirtualMethod(\"").append(cls.getClsName()).append("\", \"") + .append(jsMethodName).append("\", ").append(jsMethodName).append(");\n"); + } + } + + private static boolean appendStraightLineMethodBody(StringBuilder out, ByteCodeClass cls, BytecodeMethod method, + List instructions, String jsMethodName) { + if (!isStraightLineEligible(method, instructions)) { + return false; + } + StringBuilder setup = new StringBuilder(); + StringBuilder instructionBody = new StringBuilder(); + StringBuilder body = new StringBuilder(); + StraightLineContext ctx = new StraightLineContext(method.getMaxLocals(), method.getMaxStack()); + if (method.isStatic() && !"__CLINIT__".equals(method.getMethodName())) { + ctx.initializedClasses.add(cls.getClsName()); + } + if (!method.isStatic()) { + setup.append(" let l0 = __cn1ThisObject;\n"); + ctx.localsInitialized[0] = true; + ctx.localsUsed[0] = true; + } + List arguments = method.getArguments(); + int localIndex = method.isStatic() ? 0 : 1; + for (int i = 0; i < arguments.size(); i++) { + setup.append(" let l").append(localIndex).append(" = __cn1Arg").append(i + 1).append(";\n"); + ctx.localsInitialized[localIndex] = true; + ctx.localsUsed[localIndex] = true; + localIndex++; + if (arguments.get(i).isDoubleOrLong()) { + localIndex++; + } + } + for (int i = 0; i < instructions.size(); i++) { + Instruction instruction = instructions.get(i); + if (!appendStraightLineInstruction(instructionBody, method, instruction, ctx)) { + return false; + } + } + body.append(setup); + for (int i = 0; i < method.getMaxLocals(); i++) { + if (!ctx.localsInitialized[i] && ctx.localsUsed[i]) { + body.append(" let l").append(i).append(" = null;\n"); + } + } + for (int i = 0; i < ctx.getMaxObservedStack(); i++) { + body.append(" let s").append(i).append(" = null;\n"); + } + if (method.isSynchronizedMethod()) { + body.append(" const __cn1Monitor = ").append(method.isStatic() ? "jvm.getClassObject(\"" + cls.getClsName() + "\")" : "__cn1ThisObject").append(";\n"); + body.append(" jvm.monitorEnter(jvm.currentThread, __cn1Monitor);\n"); + body.append(" try {\n"); + } + body.append(instructionBody); + if (method.isSynchronizedMethod()) { + body.append(" } finally {\n"); + body.append(" jvm.monitorExit(jvm.currentThread, __cn1Monitor);\n"); + body.append(" }\n"); + } + out.append(body); + out.append("}\n"); + if ("__CLINIT__".equals(method.getMethodName())) { + out.append("jvm.classes[\"").append(cls.getClsName()).append("\"].clinit = ").append(jsMethodName).append(";\n"); + } + if (!method.isStatic() && !method.isConstructor()) { + out.append("jvm.addVirtualMethod(\"").append(cls.getClsName()).append("\", \"") + .append(jsMethodName).append("\", ").append(jsMethodName).append(");\n"); + } + return true; + } + + private static boolean isStraightLineEligible(BytecodeMethod method, List instructions) { + if (method.isSynchronizedMethod()) { + return false; + } + for (int i = 0; i < instructions.size(); i++) { + Instruction instruction = instructions.get(i); + if (instruction instanceof Jump || instruction instanceof SwitchInstruction || instruction instanceof TryCatch + || instruction instanceof MultiArray) { + return false; + } + if (instruction instanceof BasicInstruction) { + int opcode = ((BasicInstruction) instruction).getOpcode(); + if (opcode == Opcodes.MONITORENTER || opcode == Opcodes.MONITOREXIT || opcode == Opcodes.ATHROW) { + return false; + } + } + } + return true; + } + + private static boolean appendStraightLineInstruction(StringBuilder out, BytecodeMethod method, Instruction instruction, + StraightLineContext ctx) { + if (instruction instanceof LabelInstruction || instruction instanceof LineNumber || instruction instanceof LocalVariable) { + return true; + } + if (instruction instanceof BasicInstruction) { + return appendStraightLineBasicInstruction(out, method, (BasicInstruction) instruction, ctx); + } + if (instruction instanceof VarOp) { + return appendStraightLineVarInstruction(out, (VarOp) instruction, ctx); + } + if (instruction instanceof IInc) { + IInc iinc = (IInc) instruction; + ctx.localsUsed[iinc.getVar()] = true; + out.append(" l").append(iinc.getVar()).append(" = (l").append(iinc.getVar()).append(" || 0) + ") + .append(iinc.getAmount()).append(";\n"); + return true; + } + if (instruction instanceof Ldc) { + return appendStraightLineLdcInstruction(out, (Ldc) instruction, ctx); + } + if (instruction instanceof TypeInstruction) { + return appendStraightLineTypeInstruction(out, (TypeInstruction) instruction, ctx); + } + if (instruction instanceof Field) { + return appendStraightLineFieldInstruction(out, (Field) instruction, ctx); + } + if (instruction instanceof Invoke) { + return appendStraightLineInvokeInstruction(out, (Invoke) instruction, ctx); + } + return false; + } + + private static boolean appendStraightLineBasicInstruction(StringBuilder out, BytecodeMethod method, BasicInstruction instruction, + StraightLineContext ctx) { + switch (instruction.getOpcode()) { + case Opcodes.NOP: + return true; + case Opcodes.ACONST_NULL: + out.append(" ").append(ctx.push("null")).append(";\n"); + return true; + case Opcodes.ICONST_M1: + out.append(" ").append(ctx.push("-1")).append(";\n"); + return true; + case Opcodes.ICONST_0: + case Opcodes.ICONST_1: + case Opcodes.ICONST_2: + case Opcodes.ICONST_3: + case Opcodes.ICONST_4: + case Opcodes.ICONST_5: + out.append(" ").append(ctx.push(Integer.toString(instruction.getOpcode() - Opcodes.ICONST_0))).append(";\n"); + return true; + case Opcodes.LCONST_0: + out.append(" ").append(ctx.push("0")).append(";\n"); + return true; + case Opcodes.LCONST_1: + out.append(" ").append(ctx.push("1")).append(";\n"); + return true; + case Opcodes.FCONST_0: + case Opcodes.DCONST_0: + out.append(" ").append(ctx.push("0.0")).append(";\n"); + return true; + case Opcodes.FCONST_1: + case Opcodes.DCONST_1: + out.append(" ").append(ctx.push("1.0")).append(";\n"); + return true; + case Opcodes.FCONST_2: + out.append(" ").append(ctx.push("2.0")).append(";\n"); + return true; + case Opcodes.BIPUSH: + case Opcodes.SIPUSH: + out.append(" ").append(ctx.push(Integer.toString(instruction.getValue()))).append(";\n"); + return true; + case Opcodes.POP: + ctx.pop(); + return true; + case Opcodes.POP2: + ctx.pop(); + ctx.pop(); + return true; + case Opcodes.DUP: { + String value = ctx.peek(0); + out.append(" ").append(ctx.push(value)).append(";\n"); + return true; + } + case Opcodes.DUP_X1: { + String v1 = ctx.pop(); + String v2 = ctx.pop(); + out.append(" ").append(ctx.push(v1)).append(";\n"); + out.append(" ").append(ctx.push(v2)).append(";\n"); + out.append(" ").append(ctx.push(v1)).append(";\n"); + return true; + } + case Opcodes.DUP_X2: { + String v1 = ctx.pop(); + String v2 = ctx.pop(); + String v3 = ctx.pop(); + out.append(" ").append(ctx.push(v1)).append(";\n"); + out.append(" ").append(ctx.push(v3)).append(";\n"); + out.append(" ").append(ctx.push(v2)).append(";\n"); + out.append(" ").append(ctx.push(v1)).append(";\n"); + return true; + } + case Opcodes.DUP2: { + String v1 = ctx.pop(); + String v2 = ctx.pop(); + out.append(" ").append(ctx.push(v2)).append(";\n"); + out.append(" ").append(ctx.push(v1)).append(";\n"); + out.append(" ").append(ctx.push(v2)).append(";\n"); + out.append(" ").append(ctx.push(v1)).append(";\n"); + return true; + } + case Opcodes.DUP2_X1: { + String v1 = ctx.pop(); + String v2 = ctx.pop(); + String v3 = ctx.pop(); + out.append(" ").append(ctx.push(v2)).append(";\n"); + out.append(" ").append(ctx.push(v1)).append(";\n"); + out.append(" ").append(ctx.push(v3)).append(";\n"); + out.append(" ").append(ctx.push(v2)).append(";\n"); + out.append(" ").append(ctx.push(v1)).append(";\n"); + return true; + } + case Opcodes.DUP2_X2: { + String v1 = ctx.pop(); + String v2 = ctx.pop(); + String v3 = ctx.pop(); + String v4 = ctx.pop(); + out.append(" ").append(ctx.push(v2)).append(";\n"); + out.append(" ").append(ctx.push(v1)).append(";\n"); + out.append(" ").append(ctx.push(v4)).append(";\n"); + out.append(" ").append(ctx.push(v3)).append(";\n"); + out.append(" ").append(ctx.push(v2)).append(";\n"); + out.append(" ").append(ctx.push(v1)).append(";\n"); + return true; + } + case Opcodes.SWAP: { + String v1 = ctx.pop(); + String v2 = ctx.pop(); + out.append(" ").append(ctx.push(v1)).append(";\n"); + out.append(" ").append(ctx.push(v2)).append(";\n"); + return true; + } + case Opcodes.IADD: + return emitBinary(out, ctx, "((%s|0) + (%s|0))"); + case Opcodes.ISUB: + return emitBinary(out, ctx, "((%s|0) - (%s|0))"); + case Opcodes.IMUL: + return emitBinary(out, ctx, "((%s|0) * (%s|0))"); + case Opcodes.LADD: + case Opcodes.FADD: + case Opcodes.DADD: + return emitBinary(out, ctx, "(%s + %s)"); + case Opcodes.LSUB: + case Opcodes.FSUB: + case Opcodes.DSUB: + return emitBinary(out, ctx, "(%s - %s)"); + case Opcodes.LMUL: + case Opcodes.FMUL: + case Opcodes.DMUL: + return emitBinary(out, ctx, "(%s * %s)"); + case Opcodes.IDIV: + return emitBinary(out, ctx, "(((%s|0) / (%s|0)) | 0)"); + case Opcodes.LDIV: + return emitBinary(out, ctx, "Math.trunc(%s / %s)"); + case Opcodes.FDIV: + case Opcodes.DDIV: + return emitBinary(out, ctx, "(%s / %s)"); + case Opcodes.IREM: + return emitBinary(out, ctx, "((%s|0) %% (%s|0))"); + case Opcodes.LREM: + case Opcodes.FREM: + case Opcodes.DREM: + return emitBinary(out, ctx, "(%s %% %s)"); + case Opcodes.INEG: + return emitUnary(out, ctx, "-(%s|0)"); + case Opcodes.LNEG: + case Opcodes.FNEG: + case Opcodes.DNEG: + return emitUnary(out, ctx, "-%s"); + case Opcodes.ISHL: + return emitBinary(out, ctx, "((%s|0) << (%s & 31))"); + case Opcodes.LSHL: + return emitBinary(out, ctx, "(%s * Math.pow(2, %s & 63))"); + case Opcodes.ISHR: + return emitBinary(out, ctx, "((%s|0) >> (%s & 31))"); + case Opcodes.LSHR: + return emitBinary(out, ctx, "Math.trunc(%s / Math.pow(2, %s & 63))"); + case Opcodes.IUSHR: + return emitBinary(out, ctx, "((%s >>> (%s & 31)) | 0)"); + case Opcodes.LUSHR: + return emitBinary(out, ctx, "Math.floor((%s < 0 ? %s + 18446744073709551616 : %s) / Math.pow(2, %s & 63))", + true); + case Opcodes.IAND: + return emitBinary(out, ctx, "((%s|0) & (%s|0))"); + case Opcodes.LAND: + return emitBinary(out, ctx, "(%s & %s)"); + case Opcodes.IOR: + return emitBinary(out, ctx, "((%s|0) | (%s|0))"); + case Opcodes.LOR: + return emitBinary(out, ctx, "(%s | %s)"); + case Opcodes.IXOR: + return emitBinary(out, ctx, "((%s|0) ^ (%s|0))"); + case Opcodes.LXOR: + return emitBinary(out, ctx, "(%s ^ %s)"); + case Opcodes.I2L: + case Opcodes.I2F: + case Opcodes.I2D: + case Opcodes.L2F: + case Opcodes.L2D: + case Opcodes.F2D: + case Opcodes.D2F: + return true; + case Opcodes.I2B: + return emitUnary(out, ctx, "((%s << 24) >> 24)"); + case Opcodes.I2C: + return emitUnary(out, ctx, "(%s & 65535)"); + case Opcodes.I2S: + return emitUnary(out, ctx, "((%s << 16) >> 16)"); + case Opcodes.L2I: + case Opcodes.F2I: + case Opcodes.D2I: + return emitUnary(out, ctx, "(%s | 0)"); + case Opcodes.F2L: + case Opcodes.D2L: + return true; + case Opcodes.LCMP: + return emitBinary(out, ctx, "(%s < %s ? -1 : (%s > %s ? 1 : 0))", true); + case Opcodes.FCMPL: + case Opcodes.DCMPL: + return emitBinary(out, ctx, "((isNaN(%s) || isNaN(%s)) ? -1 : (%s < %s ? -1 : (%s > %s ? 1 : 0)))", true); + case Opcodes.FCMPG: + case Opcodes.DCMPG: + return emitBinary(out, ctx, "((isNaN(%s) || isNaN(%s)) ? 1 : (%s < %s ? -1 : (%s > %s ? 1 : 0)))", true); + case Opcodes.IRETURN: + case Opcodes.ARETURN: + case Opcodes.LRETURN: + case Opcodes.FRETURN: + case Opcodes.DRETURN: + out.append(" return ").append(ctx.pop()).append(";\n"); + return true; + case Opcodes.RETURN: + out.append(" return null;\n"); + return true; + case Opcodes.ARRAYLENGTH: + return emitUnary(out, ctx, "%s.length"); + case Opcodes.AALOAD: + case Opcodes.IALOAD: + case Opcodes.LALOAD: + case Opcodes.FALOAD: + case Opcodes.DALOAD: + case Opcodes.BALOAD: + case Opcodes.CALOAD: + case Opcodes.SALOAD: { + String idx = ctx.pop(); + String arr = ctx.pop(); + String arrayTemp = ctx.nextTemp("__arr"); + String indexTemp = ctx.nextTemp("__idx"); + out.append(" { const ").append(arrayTemp).append(" = ").append(arr) + .append("; const ").append(indexTemp).append(" = ").append(idx) + .append("; if (!").append(arrayTemp).append(".__array) throw new Error(\"Array expected\"); if (") + .append(indexTemp).append(" < 0 || ").append(indexTemp).append(" >= ").append(arrayTemp) + .append(".length) throw new Error(\"ArrayIndexOutOfBoundsException\"); ") + .append(ctx.push(arrayTemp + "[" + indexTemp + "]")).append("; }\n"); + return true; + } + case Opcodes.AASTORE: + case Opcodes.IASTORE: + case Opcodes.LASTORE: + case Opcodes.FASTORE: + case Opcodes.DASTORE: + case Opcodes.BASTORE: + case Opcodes.CASTORE: + case Opcodes.SASTORE: { + String value = ctx.pop(); + String idx = ctx.pop(); + String arr = ctx.pop(); + String arrayTemp = ctx.nextTemp("__arr"); + String indexTemp = ctx.nextTemp("__idx"); + out.append(" { const ").append(arrayTemp).append(" = ").append(arr) + .append("; const ").append(indexTemp).append(" = ").append(idx) + .append("; if (!").append(arrayTemp).append(".__array) throw new Error(\"Array expected\"); if (") + .append(indexTemp).append(" < 0 || ").append(indexTemp).append(" >= ").append(arrayTemp) + .append(".length) throw new Error(\"ArrayIndexOutOfBoundsException\"); ") + .append(arrayTemp).append("[").append(indexTemp).append("] = ").append(value).append("; }\n"); + return true; + } + default: + return false; + } + } + + private static boolean emitBinary(StringBuilder out, StraightLineContext ctx, String format) { + return emitBinary(out, ctx, format, false); + } + + private static boolean emitBinary(StringBuilder out, StraightLineContext ctx, String format, boolean repeatedArgs) { + String b = ctx.pop(); + String a = ctx.pop(); + String expr; + if (repeatedArgs) { + expr = String.format(format, a, b, a, b, a, b); + } else { + expr = String.format(format, a, b); + } + out.append(" ").append(ctx.push(expr)).append(";\n"); + return true; + } + + private static boolean emitUnary(StringBuilder out, StraightLineContext ctx, String format) { + String value = ctx.pop(); + out.append(" ").append(ctx.push(String.format(format, value))).append(";\n"); + return true; + } + + private static boolean appendStraightLineVarInstruction(StringBuilder out, VarOp instruction, StraightLineContext ctx) { + switch (instruction.getOpcode()) { + case Opcodes.BIPUSH: + case Opcodes.SIPUSH: + out.append(" ").append(ctx.push(Integer.toString(instruction.getIndex()))).append(";\n"); + return true; + case Opcodes.NEWARRAY: { + String size = ctx.pop(); + out.append(" ").append(ctx.push("jvm.newArray(" + size + ", \"" + primitiveArrayType(instruction.getIndex()) + "\", 1)")).append(";\n"); + return true; + } + case Opcodes.ILOAD: + case Opcodes.LLOAD: + case Opcodes.FLOAD: + case Opcodes.DLOAD: + case Opcodes.ALOAD: + ctx.localsUsed[instruction.getIndex()] = true; + out.append(" ").append(ctx.push("l" + instruction.getIndex())).append(";\n"); + return true; + case Opcodes.ISTORE: + case Opcodes.LSTORE: + case Opcodes.FSTORE: + case Opcodes.DSTORE: + case Opcodes.ASTORE: + ctx.localsUsed[instruction.getIndex()] = true; + out.append(" l").append(instruction.getIndex()).append(" = ").append(ctx.pop()).append(";\n"); + return true; + default: + return false; + } + } + + private static boolean appendStraightLineLdcInstruction(StringBuilder out, Ldc instruction, StraightLineContext ctx) { + Object value = instruction.getValue(); + if (value instanceof String) { + out.append(" ").append(ctx.push("jvm.createStringLiteral(\"" + JavascriptNameUtil.escapeJs((String) value) + "\")")).append(";\n"); + return true; + } + if (value instanceof Integer || value instanceof Long || value instanceof Float || value instanceof Double) { + out.append(" ").append(ctx.push(value.toString())).append(";\n"); + return true; + } + if (value instanceof Type) { + Type type = (Type) value; + if (type.getSort() == Type.OBJECT) { + out.append(" ").append(ctx.push("jvm.getClassObject(\"" + JavascriptNameUtil.sanitizeClassName(type.getInternalName()) + "\")")).append(";\n"); + return true; + } + } + return false; + } + + private static boolean appendStraightLineTypeInstruction(StringBuilder out, TypeInstruction instruction, StraightLineContext ctx) { + String typeName = JavascriptNameUtil.runtimeTypeName(instruction.getTypeName()); + switch (instruction.getOpcode()) { + case Opcodes.NEW: + out.append(" ").append(ctx.push("jvm.newObject(\"" + typeName + "\")")).append(";\n"); + return true; + case Opcodes.ANEWARRAY: { + String size = ctx.pop(); + out.append(" ").append(ctx.push("jvm.newArray(" + size + ", \"" + typeName + "\", 1)")).append(";\n"); + return true; + } + case Opcodes.CHECKCAST: { + String value = ctx.peek(0); + out.append(" if (").append(value).append(" != null && !jvm.instanceOf(").append(value).append(", \"") + .append(typeName).append("\")) throw new Error(\"ClassCastException\");\n"); + return true; + } + case Opcodes.INSTANCEOF: { + String value = ctx.pop(); + out.append(" ").append(ctx.push("(jvm.instanceOf(" + value + ", \"" + typeName + "\") ? 1 : 0)")).append(";\n"); + return true; + } + default: + return false; + } + } + + private static boolean appendStraightLineFieldInstruction(StringBuilder out, Field field, StraightLineContext ctx) { + String owner = JavascriptNameUtil.sanitizeClassName(field.getOwner()); + String fieldName = field.getFieldName(); + String propertyName = JavascriptNameUtil.fieldProperty(field.getOwner(), fieldName); + switch (field.getOpcode()) { + case Opcodes.GETSTATIC: + appendStraightLineEnsureClassInitialized(out, ctx, owner); + out.append(" ").append(ctx.push("jvm.classes[\"" + owner + "\"].staticFields[\"" + fieldName + "\"]")).append(";\n"); + return true; + case Opcodes.PUTSTATIC: + appendStraightLineEnsureClassInitialized(out, ctx, owner); + out.append(" jvm.classes[\"").append(owner).append("\"].staticFields[\"").append(fieldName).append("\"] = ") + .append(ctx.pop()).append(";\n"); + return true; + case Opcodes.GETFIELD: { + String target = ctx.pop(); + out.append(" ").append(ctx.push(target + "[\"" + propertyName + "\"]")).append(";\n"); + return true; + } + case Opcodes.PUTFIELD: { + String value = ctx.pop(); + String target = ctx.pop(); + out.append(" ").append(target).append("[\"").append(propertyName).append("\"] = ").append(value).append(";\n"); + return true; + } + default: + return false; + } + } + + private static void appendStraightLineEnsureClassInitialized(StringBuilder out, StraightLineContext ctx, String owner) { + if (ctx.initializedClasses.add(owner)) { + out.append(" jvm.ensureClassInitialized(\"").append(owner).append("\");\n"); + } + } + + private static boolean appendStraightLineInvokeInstruction(StringBuilder out, Invoke invoke, StraightLineContext ctx) { + String methodId = JavascriptNameUtil.methodIdentifier(invoke.getOwner(), invoke.getName(), invoke.getDesc()); + List args = JavascriptNameUtil.argumentTypes(invoke.getDesc()); + boolean hasReturn = invoke.getDesc().charAt(invoke.getDesc().length() - 1) != 'V'; + String[] argValues = new String[args.size()]; + for (int i = args.size() - 1; i >= 0; i--) { + argValues[i] = ctx.pop(); + } + String target = null; + switch (invoke.getOpcode()) { + case Opcodes.INVOKEVIRTUAL: + case Opcodes.INVOKEINTERFACE: + case Opcodes.INVOKESPECIAL: + target = ctx.pop(); + break; + case Opcodes.INVOKESTATIC: + break; + default: + return false; + } + if (invoke.getOpcode() == Opcodes.INVOKEVIRTUAL || invoke.getOpcode() == Opcodes.INVOKEINTERFACE) { + out.append(" {\n"); + out.append(" const __target = ").append(target).append(";\n"); + out.append(" const __method = ((jvm.classes[__target.__class] && jvm.classes[__target.__class].methods) ? jvm.classes[__target.__class].methods[\"").append(methodId) + .append("\"] : null) || jvm.resolveVirtual(__target.__class, \"").append(methodId).append("\");\n"); + if (hasReturn) { + out.append(" const __result = yield* __method("); + appendInvocationArgumentExpressions(out, "__target", argValues); + out.append(");\n"); + out.append(" ").append(ctx.push("__result")).append(";\n"); + } else { + out.append(" yield* __method("); + appendInvocationArgumentExpressions(out, "__target", argValues); + out.append(");\n"); + } + out.append(" }\n"); + return true; + } + if (hasReturn) { + out.append(" { const __result = yield* ").append(methodId).append("("); + } else { + out.append(" { yield* ").append(methodId).append("("); + } + appendInvocationArgumentExpressions(out, target, argValues); + out.append(");"); + if (hasReturn) { + out.append(" ").append(ctx.push("__result")).append(";"); + } + out.append(" }\n"); + return true; + } + + private static void appendInvocationArgumentExpressions(StringBuilder out, String target, String[] args) { + boolean first = true; + if (target != null) { + out.append(target); + first = false; + } + for (int i = 0; i < args.length; i++) { + if (!first) { + out.append(", "); + } + first = false; + out.append(args[i]); + } + } + + private static final class StraightLineContext { + private final boolean[] localsInitialized; + private final boolean[] localsUsed; + private final Set initializedClasses; + private int sp; + private int maxObservedStack; + private int nextTempId; + + private StraightLineContext(int maxLocals, int maxStack) { + this.localsInitialized = new boolean[Math.max(1, maxLocals)]; + this.localsUsed = new boolean[Math.max(1, maxLocals)]; + this.initializedClasses = new HashSet(); + this.sp = 0; + this.maxObservedStack = 0; + this.nextTempId = 0; + } + + private String push(String expression) { + String slot = "s" + sp++; + if (sp > maxObservedStack) { + maxObservedStack = sp; + } + return slot + " = " + expression; + } + + private String pop() { + sp--; + if (sp < 0) { + throw new IllegalStateException("Straight-line JS lowering stack underflow"); + } + return "s" + sp; + } + + private String peek(int depth) { + int index = sp - 1 - depth; + if (index < 0) { + throw new IllegalStateException("Straight-line JS lowering stack underflow"); + } + return "s" + index; + } + + private int getMaxObservedStack() { + return maxObservedStack; + } + + private String nextTemp(String prefix) { + return prefix + (nextTempId++); + } + } + + private static void appendTryCatchTable(StringBuilder out, List instructions, Map labelToIndex) { + out.append(" const __cn1TryCatch = ["); + boolean first = true; + for (int i = 0; i < instructions.size(); i++) { + Instruction instruction = instructions.get(i); + if (!(instruction instanceof TryCatch)) { + continue; + } + TryCatch tryCatch = (TryCatch) instruction; + if (!first) { + out.append(", "); + } + first = false; + out.append("{ start: ").append(resolveLabelIndex(labelToIndex, tryCatch.getStart(), "try start")); + out.append(", end: ").append(resolveLabelIndex(labelToIndex, tryCatch.getEnd(), "try end")); + out.append(", handler: ").append(resolveLabelIndex(labelToIndex, tryCatch.getHandler(), "try handler")); + out.append(", type: "); + if (tryCatch.getType() == null) { + out.append("null"); + } else { + out.append("\"").append(JavascriptNameUtil.runtimeTypeName(tryCatch.getType())).append("\""); + } + out.append("}"); + } + out.append("];\n"); + } + + private static String jsMethodIdentifier(ByteCodeClass cls, BytecodeMethod method) { + return JavascriptNameUtil.methodIdentifier(cls.getClsName(), method.getMethodName(), method.getSignature()); + } + + private static void appendNativeStubIfNeeded(StringBuilder out, ByteCodeClass cls, BytecodeMethod method) { + String jsMethodName = jsMethodIdentifier(cls, method); + JavascriptNativeRegistry.NativeCategory category = JavascriptNativeRegistry.categoryFor(jsMethodName); + if (category == JavascriptNativeRegistry.NativeCategory.RUNTIME_IMPLEMENTED) { + return; + } + String reason = JavascriptNativeRegistry.unsupportedReason(jsMethodName); + out.append("if (typeof ").append(jsMethodName).append(" === \"undefined\") {\n"); + out.append(" ").append(jsMethodName).append(" = function*("); + boolean first = true; + if (!method.isStatic()) { + out.append("__cn1ThisObject"); + first = false; + } + List arguments = method.getArguments(); + for (int i = 0; i < arguments.size(); i++) { + if (!first) { + out.append(", "); + } + first = false; + out.append("__cn1Arg").append(i + 1); + } + out.append(") { "); + if (category == JavascriptNativeRegistry.NativeCategory.HOST_HOOK) { + out.append("return yield jvm.invokeHostNative(\"").append(jsMethodName).append("\", ["); + boolean firstArg = true; + if (!method.isStatic()) { + out.append("__cn1ThisObject"); + firstArg = false; + } + for (int i = 0; i < arguments.size(); i++) { + if (!firstArg) { + out.append(", "); + } + firstArg = false; + out.append("__cn1Arg").append(i + 1); + } + out.append("]);"); + } else { + out.append("throw new Error(\""); + if (reason == null) { + out.append("Missing javascript native method ").append(jsMethodName); + } else { + out.append(JavascriptNameUtil.escapeJs(reason)); + } + out.append("\");"); + } + out.append(" };\n"); + out.append("}\n"); + } + + private static Map buildLabelMap(List instructions) { + Map out = new HashMap(); + for (int i = 0; i < instructions.size(); i++) { + Instruction instruction = instructions.get(i); + if (instruction instanceof LabelInstruction) { + out.put(((LabelInstruction) instruction).getLabel(), Integer.valueOf(i)); + } + } + return out; + } + + private static void appendInstruction(StringBuilder out, BytecodeMethod method, List allInstructions, + Map labelToIndex, Instruction instruction, int index) { + if (instruction instanceof LabelInstruction || instruction instanceof LineNumber || instruction instanceof LocalVariable + || instruction instanceof TryCatch) { + out.append(" pc = ").append(index + 1).append("; break;\n"); + return; + } + if (instruction instanceof BasicInstruction) { + appendBasicInstruction(out, method, (BasicInstruction) instruction, index); + return; + } + if (instruction instanceof VarOp) { + appendVarInstruction(out, (VarOp) instruction, index); + return; + } + if (instruction instanceof IInc) { + IInc iinc = (IInc) instruction; + out.append(" locals[").append(iinc.getVar()).append("] = (locals[").append(iinc.getVar()) + .append("] || 0) + ").append(iinc.getAmount()).append(";\n"); + out.append(" pc = ").append(index + 1).append("; break;\n"); + return; + } + if (instruction instanceof Ldc) { + appendLdcInstruction(out, (Ldc) instruction, index); + return; + } + if (instruction instanceof TypeInstruction) { + appendTypeInstruction(out, (TypeInstruction) instruction, index); + return; + } + if (instruction instanceof Field) { + appendFieldInstruction(out, (Field) instruction, index); + return; + } + if (instruction instanceof Jump) { + appendJumpInstruction(out, (Jump) instruction, labelToIndex, index); + return; + } + if (instruction instanceof Invoke) { + appendInvokeInstruction(out, (Invoke) instruction, index); + return; + } + if (instruction instanceof SwitchInstruction) { + appendSwitchInstruction(out, (SwitchInstruction) instruction, labelToIndex, index); + return; + } + if (instruction instanceof MultiArray) { + appendMultiArrayInstruction(out, (MultiArray) instruction, index); + return; + } + throw new IllegalArgumentException("Unsupported instruction type in javascript output: " + + instruction.getClass().getName() + " for " + method.getMethodIdentifier()); + } + + private static void appendMultiArrayInstruction(StringBuilder out, MultiArray instruction, int index) { + String desc = instruction.getDesc(); + int totalDimensions = arrayDescriptorDimensions(desc); + String componentType = arrayDescriptorComponent(desc); + int allocatedDimensions = instruction.getDimensionsToAllocate(); + out.append(" { const sizes = new Array(").append(totalDimensions).append(");"); + out.append(" for (let i = ").append(allocatedDimensions - 1).append("; i >= 0; i--) { sizes[i] = stack.pop() | 0; }"); + out.append(" for (let i = ").append(allocatedDimensions).append("; i < ").append(totalDimensions) + .append("; i++) { sizes[i] = -1; }"); + out.append(" stack.push(jvm.newMultiArray(sizes, \"").append(componentType).append("\", ") + .append(totalDimensions).append(")); pc = ").append(index + 1).append("; break; }\n"); + } + + private static int arrayDescriptorDimensions(String desc) { + int dimensions = 0; + while (dimensions < desc.length() && desc.charAt(dimensions) == '[') { + dimensions++; + } + return dimensions; + } + + private static String arrayDescriptorComponent(String desc) { + int dimensions = arrayDescriptorDimensions(desc); + char kind = desc.charAt(dimensions); + if (kind == 'L') { + return JavascriptNameUtil.sanitizeClassName(desc.substring(dimensions + 1, desc.length() - 1)); + } + switch (kind) { + case 'Z': + return "JAVA_BOOLEAN"; + case 'C': + return "JAVA_CHAR"; + case 'F': + return "JAVA_FLOAT"; + case 'D': + return "JAVA_DOUBLE"; + case 'B': + return "JAVA_BYTE"; + case 'S': + return "JAVA_SHORT"; + case 'I': + return "JAVA_INT"; + case 'J': + return "JAVA_LONG"; + default: + throw new IllegalArgumentException("Unsupported MULTIANEWARRAY descriptor " + desc); + } + } + + private static void appendSwitchInstruction(StringBuilder out, SwitchInstruction instruction, Map labelToIndex, int index) { + out.append(" const __switchValue = stack.pop() | 0;\n"); + out.append(" switch (__switchValue) {\n"); + int[] keys = instruction.getKeys(); + Label[] labels = instruction.getLabels(); + for (int i = 0; i < keys.length; i++) { + out.append(" case ").append(keys[i]).append(": pc = ") + .append(resolveLabelIndex(labelToIndex, labels[i], "switch case")).append("; break;\n"); + } + Label defaultLabel = instruction.getDefaultLabel(); + if (defaultLabel != null) { + out.append(" default: pc = ").append(resolveLabelIndex(labelToIndex, defaultLabel, "switch default")).append("; break;\n"); + } else { + out.append(" default: pc = ").append(index + 1).append("; break;\n"); + } + out.append(" }\n"); + out.append(" break;\n"); + } + + private static int resolveLabelIndex(Map labelToIndex, Label label, String context) { + Integer target = labelToIndex.get(label); + if (target == null) { + throw new IllegalStateException("Missing label target for " + context + " in JS backend"); + } + return target.intValue(); + } + + private static void appendBasicInstruction(StringBuilder out, BytecodeMethod method, BasicInstruction instruction, int index) { + switch (instruction.getOpcode()) { + case Opcodes.NOP: + out.append(" pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.ACONST_NULL: + out.append(" stack.push(null); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.ICONST_M1: + out.append(" stack.push(-1); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.ICONST_0: + case Opcodes.ICONST_1: + case Opcodes.ICONST_2: + case Opcodes.ICONST_3: + case Opcodes.ICONST_4: + case Opcodes.ICONST_5: + out.append(" stack.push(").append(instruction.getOpcode() - Opcodes.ICONST_0).append("); pc = ") + .append(index + 1).append("; break;\n"); + return; + case Opcodes.LCONST_0: + out.append(" stack.push(0); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.LCONST_1: + out.append(" stack.push(1); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.FCONST_0: + case Opcodes.DCONST_0: + out.append(" stack.push(0.0); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.FCONST_1: + case Opcodes.DCONST_1: + out.append(" stack.push(1.0); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.FCONST_2: + out.append(" stack.push(2.0); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.BIPUSH: + case Opcodes.SIPUSH: + out.append(" stack.push(").append(instruction.getValue()).append("); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.POP: + out.append(" stack.pop(); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.POP2: + out.append(" stack.pop(); stack.pop(); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.DUP: + out.append(" stack.push(stack[stack.length - 1]); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.DUP_X1: + out.append(" { const v1 = stack.pop(); const v2 = stack.pop(); stack.push(v1); stack.push(v2); stack.push(v1); pc = ") + .append(index + 1).append("; break; }\n"); + return; + case Opcodes.DUP_X2: + out.append(" { const v1 = stack.pop(); const v2 = stack.pop(); const v3 = stack.pop(); stack.push(v1); stack.push(v3); stack.push(v2); stack.push(v1); pc = ") + .append(index + 1).append("; break; }\n"); + return; + case Opcodes.DUP2: + out.append(" { const v1 = stack.pop(); const v2 = stack.pop(); stack.push(v2); stack.push(v1); stack.push(v2); stack.push(v1); pc = ") + .append(index + 1).append("; break; }\n"); + return; + case Opcodes.DUP2_X1: + out.append(" { const v1 = stack.pop(); const v2 = stack.pop(); const v3 = stack.pop(); stack.push(v2); stack.push(v1); stack.push(v3); stack.push(v2); stack.push(v1); pc = ") + .append(index + 1).append("; break; }\n"); + return; + case Opcodes.DUP2_X2: + out.append(" { const v1 = stack.pop(); const v2 = stack.pop(); const v3 = stack.pop(); const v4 = stack.pop(); stack.push(v2); stack.push(v1); stack.push(v4); stack.push(v3); stack.push(v2); stack.push(v1); pc = ") + .append(index + 1).append("; break; }\n"); + return; + case Opcodes.SWAP: + out.append(" { const v1 = stack.pop(); const v2 = stack.pop(); stack.push(v1); stack.push(v2); pc = ") + .append(index + 1).append("; break; }\n"); + return; + case Opcodes.IADD: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push((a|0) + (b|0)); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.ISUB: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push((a|0) - (b|0)); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.IMUL: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push((a|0) * (b|0)); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.LADD: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push(a + b); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.LSUB: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push(a - b); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.LMUL: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push(a * b); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.FADD: + case Opcodes.DADD: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push(a + b); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.FSUB: + case Opcodes.DSUB: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push(a - b); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.FMUL: + case Opcodes.DMUL: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push(a * b); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.IDIV: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push(((a|0) / (b|0)) | 0); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.LDIV: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push(Math.trunc(a / b)); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.FDIV: + case Opcodes.DDIV: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push(a / b); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.IREM: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push((a|0) % (b|0)); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.LREM: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push(a % b); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.FREM: + case Opcodes.DREM: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push(a % b); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.INEG: + out.append(" stack.push(-(stack.pop()|0)); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.LNEG: + out.append(" stack.push(-stack.pop()); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.FNEG: + case Opcodes.DNEG: + out.append(" stack.push(-stack.pop()); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.ISHL: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push((a|0) << (b & 31)); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.LSHL: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push(a * Math.pow(2, b & 63)); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.ISHR: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push((a|0) >> (b & 31)); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.LSHR: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push(Math.trunc(a / Math.pow(2, b & 63))); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.IUSHR: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push((a >>> (b & 31)) | 0); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.LUSHR: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push(Math.floor((a < 0 ? a + 18446744073709551616 : a) / Math.pow(2, b & 63))); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.IAND: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push((a|0) & (b|0)); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.LAND: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push(a & b); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.IOR: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push((a|0) | (b|0)); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.LOR: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push(a | b); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.IXOR: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push((a|0) ^ (b|0)); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.LXOR: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push(a ^ b); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.I2L: + case Opcodes.F2D: + case Opcodes.D2F: + out.append(" pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.I2B: + out.append(" stack.push((stack.pop() << 24) >> 24); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.I2C: + out.append(" stack.push(stack.pop() & 65535); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.I2S: + out.append(" stack.push((stack.pop() << 16) >> 16); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.L2I: + case Opcodes.F2I: + case Opcodes.D2I: + out.append(" stack.push(stack.pop() | 0); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.I2F: + case Opcodes.I2D: + case Opcodes.L2F: + case Opcodes.L2D: + case Opcodes.F2L: + case Opcodes.D2L: + out.append(" pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.LCMP: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push(a < b ? -1 : (a > b ? 1 : 0)); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.FCMPL: + case Opcodes.DCMPL: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push((isNaN(a) || isNaN(b)) ? -1 : (a < b ? -1 : (a > b ? 1 : 0))); pc = ") + .append(index + 1).append("; break; }\n"); + return; + case Opcodes.FCMPG: + case Opcodes.DCMPG: + out.append(" { const b = stack.pop(); const a = stack.pop(); stack.push((isNaN(a) || isNaN(b)) ? 1 : (a < b ? -1 : (a > b ? 1 : 0))); pc = ") + .append(index + 1).append("; break; }\n"); + return; + case Opcodes.IRETURN: + case Opcodes.ARETURN: + case Opcodes.LRETURN: + case Opcodes.FRETURN: + case Opcodes.DRETURN: + out.append(" return stack.pop();\n"); + return; + case Opcodes.ATHROW: + out.append(" throw stack.pop();\n"); + return; + case Opcodes.RETURN: + out.append(" return null;\n"); + return; + case Opcodes.ARRAYLENGTH: + out.append(" { const arr = stack.pop(); stack.push(arr.length); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.AALOAD: + case Opcodes.IALOAD: + case Opcodes.LALOAD: + case Opcodes.FALOAD: + case Opcodes.DALOAD: + case Opcodes.BALOAD: + case Opcodes.CALOAD: + case Opcodes.SALOAD: + out.append(" { const idx = stack.pop(); const arr = stack.pop(); if (!arr.__array) throw new Error(\"Array expected\"); if (idx < 0 || idx >= arr.length) throw new Error(\"ArrayIndexOutOfBoundsException\"); stack.push(arr[idx]); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.AASTORE: + case Opcodes.IASTORE: + case Opcodes.LASTORE: + case Opcodes.FASTORE: + case Opcodes.DASTORE: + case Opcodes.BASTORE: + case Opcodes.CASTORE: + case Opcodes.SASTORE: + out.append(" { const value = stack.pop(); const idx = stack.pop(); const arr = stack.pop(); if (!arr.__array) throw new Error(\"Array expected\"); if (idx < 0 || idx >= arr.length) throw new Error(\"ArrayIndexOutOfBoundsException\"); arr[idx] = value; pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.MONITORENTER: + out.append(" jvm.monitorEnter(jvm.currentThread, stack.pop()); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.MONITOREXIT: + out.append(" jvm.monitorExit(jvm.currentThread, stack.pop()); pc = ").append(index + 1).append("; break;\n"); + return; + default: + throw new IllegalArgumentException("Unsupported basic opcode " + instruction.getOpcode() + + " in " + method.getMethodIdentifier()); + } + } + + private static void appendVarInstruction(StringBuilder out, VarOp instruction, int index) { + switch (instruction.getOpcode()) { + case Opcodes.BIPUSH: + case Opcodes.SIPUSH: + out.append(" stack.push(").append(instruction.getIndex()).append("); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.NEWARRAY: + out.append(" { const size = stack.pop(); stack.push(jvm.newArray(size, \"") + .append(primitiveArrayType(instruction.getIndex())).append("\", 1)); pc = ") + .append(index + 1).append("; break; }\n"); + return; + case Opcodes.ILOAD: + case Opcodes.LLOAD: + case Opcodes.FLOAD: + case Opcodes.DLOAD: + case Opcodes.ALOAD: + out.append(" stack.push(locals[").append(instruction.getIndex()).append("]); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.ISTORE: + case Opcodes.LSTORE: + case Opcodes.FSTORE: + case Opcodes.DSTORE: + case Opcodes.ASTORE: + out.append(" locals[").append(instruction.getIndex()).append("] = stack.pop(); pc = ").append(index + 1).append("; break;\n"); + return; + default: + throw new IllegalArgumentException("Unsupported var opcode " + instruction.getOpcode()); + } + } + + private static String primitiveArrayType(int operand) { + switch (operand) { + case Opcodes.T_BOOLEAN: + return "JAVA_BOOLEAN"; + case Opcodes.T_CHAR: + return "JAVA_CHAR"; + case Opcodes.T_FLOAT: + return "JAVA_FLOAT"; + case Opcodes.T_DOUBLE: + return "JAVA_DOUBLE"; + case Opcodes.T_BYTE: + return "JAVA_BYTE"; + case Opcodes.T_SHORT: + return "JAVA_SHORT"; + case Opcodes.T_INT: + return "JAVA_INT"; + case Opcodes.T_LONG: + return "JAVA_LONG"; + default: + throw new IllegalArgumentException("Unsupported NEWARRAY operand " + operand); + } + } + + private static void appendLdcInstruction(StringBuilder out, Ldc instruction, int index) { + Object value = instruction.getValue(); + if (value instanceof String) { + out.append(" stack.push(jvm.createStringLiteral(\"") + .append(JavascriptNameUtil.escapeJs((String) value)).append("\")); pc = ").append(index + 1).append("; break;\n"); + return; + } + if (value instanceof Integer || value instanceof Long || value instanceof Float || value instanceof Double) { + out.append(" stack.push(").append(value.toString()).append("); pc = ").append(index + 1).append("; break;\n"); + return; + } + if (value instanceof Type) { + Type type = (Type) value; + if (type.getSort() == Type.OBJECT) { + out.append(" stack.push(jvm.getClassObject(\"").append(JavascriptNameUtil.sanitizeClassName(type.getInternalName())) + .append("\")); pc = ").append(index + 1).append("; break;\n"); + return; + } + } + throw new IllegalArgumentException("Unsupported ldc constant in javascript backend: " + value); + } + + private static void appendTypeInstruction(StringBuilder out, TypeInstruction instruction, int index) { + String typeName = JavascriptNameUtil.runtimeTypeName(instruction.getTypeName()); + switch (instruction.getOpcode()) { + case Opcodes.NEW: + out.append(" stack.push(jvm.newObject(\"").append(typeName).append("\")); pc = ").append(index + 1).append("; break;\n"); + return; + case Opcodes.ANEWARRAY: + out.append(" { const size = stack.pop(); stack.push(jvm.newArray(size, \"").append(typeName) + .append("\", 1)); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.CHECKCAST: + out.append(" { const value = stack[stack.length - 1]; if (value != null && !jvm.instanceOf(value, \"") + .append(typeName) + .append("\")) throw new Error(\"ClassCastException\"); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.INSTANCEOF: + out.append(" { const value = stack.pop(); stack.push(jvm.instanceOf(value, \"").append(typeName) + .append("\") ? 1 : 0); pc = ").append(index + 1).append("; break; }\n"); + return; + default: + throw new IllegalArgumentException("Unsupported type opcode " + instruction.getOpcode()); + } + } + + private static void appendFieldInstruction(StringBuilder out, Field field, int index) { + String owner = JavascriptNameUtil.sanitizeClassName(field.getOwner()); + String fieldName = field.getFieldName(); + String propertyName = JavascriptNameUtil.fieldProperty(field.getOwner(), fieldName); + switch (field.getOpcode()) { + case Opcodes.GETSTATIC: + out.append(" jvm.ensureClassInitialized(\"").append(owner).append("\"); stack.push(jvm.classes[\"") + .append(owner).append("\"].staticFields[\"").append(fieldName).append("\"]); pc = ") + .append(index + 1).append("; break;\n"); + return; + case Opcodes.PUTSTATIC: + out.append(" jvm.ensureClassInitialized(\"").append(owner).append("\"); jvm.classes[\"") + .append(owner).append("\"].staticFields[\"").append(fieldName).append("\"] = stack.pop(); pc = ") + .append(index + 1).append("; break;\n"); + return; + case Opcodes.GETFIELD: + out.append(" { const target = stack.pop(); stack.push(target[\"").append(propertyName) + .append("\"]); pc = ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.PUTFIELD: + out.append(" { const value = stack.pop(); const target = stack.pop(); target[\"").append(propertyName) + .append("\"] = value; pc = ").append(index + 1).append("; break; }\n"); + return; + default: + throw new IllegalArgumentException("Unsupported field opcode " + field.getOpcode()); + } + } + + private static void appendJumpInstruction(StringBuilder out, Jump jump, Map labelToIndex, int index) { + Integer target = labelToIndex.get(jump.getLabel()); + if (target == null) { + throw new IllegalStateException("Missing label target for jump in JS backend"); + } + switch (jump.getOpcode()) { + case Opcodes.GOTO: + out.append(" pc = ").append(target.intValue()).append("; break;\n"); + return; + case Opcodes.IFEQ: + out.append(" pc = ((stack.pop()|0) == 0) ? ").append(target.intValue()).append(" : ").append(index + 1).append("; break;\n"); + return; + case Opcodes.IFNE: + out.append(" pc = ((stack.pop()|0) != 0) ? ").append(target.intValue()).append(" : ").append(index + 1).append("; break;\n"); + return; + case Opcodes.IFLT: + out.append(" pc = ((stack.pop()|0) < 0) ? ").append(target.intValue()).append(" : ").append(index + 1).append("; break;\n"); + return; + case Opcodes.IFLE: + out.append(" pc = ((stack.pop()|0) <= 0) ? ").append(target.intValue()).append(" : ").append(index + 1).append("; break;\n"); + return; + case Opcodes.IFGT: + out.append(" pc = ((stack.pop()|0) > 0) ? ").append(target.intValue()).append(" : ").append(index + 1).append("; break;\n"); + return; + case Opcodes.IFGE: + out.append(" pc = ((stack.pop()|0) >= 0) ? ").append(target.intValue()).append(" : ").append(index + 1).append("; break;\n"); + return; + case Opcodes.IFNULL: + out.append(" pc = (stack.pop() == null) ? ").append(target.intValue()).append(" : ").append(index + 1).append("; break;\n"); + return; + case Opcodes.IFNONNULL: + out.append(" pc = (stack.pop() != null) ? ").append(target.intValue()).append(" : ").append(index + 1).append("; break;\n"); + return; + case Opcodes.IF_ICMPEQ: + out.append(" { const b = stack.pop(); const a = stack.pop(); pc = ((a|0) == (b|0)) ? ").append(target.intValue()).append(" : ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.IF_ICMPNE: + out.append(" { const b = stack.pop(); const a = stack.pop(); pc = ((a|0) != (b|0)) ? ").append(target.intValue()).append(" : ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.IF_ICMPLT: + out.append(" { const b = stack.pop(); const a = stack.pop(); pc = ((a|0) < (b|0)) ? ").append(target.intValue()).append(" : ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.IF_ICMPLE: + out.append(" { const b = stack.pop(); const a = stack.pop(); pc = ((a|0) <= (b|0)) ? ").append(target.intValue()).append(" : ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.IF_ICMPGT: + out.append(" { const b = stack.pop(); const a = stack.pop(); pc = ((a|0) > (b|0)) ? ").append(target.intValue()).append(" : ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.IF_ICMPGE: + out.append(" { const b = stack.pop(); const a = stack.pop(); pc = ((a|0) >= (b|0)) ? ").append(target.intValue()).append(" : ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.IF_ACMPEQ: + out.append(" { const b = stack.pop(); const a = stack.pop(); pc = (a === b) ? ").append(target.intValue()).append(" : ").append(index + 1).append("; break; }\n"); + return; + case Opcodes.IF_ACMPNE: + out.append(" { const b = stack.pop(); const a = stack.pop(); pc = (a !== b) ? ").append(target.intValue()).append(" : ").append(index + 1).append("; break; }\n"); + return; + default: + throw new IllegalArgumentException("Unsupported jump opcode " + jump.getOpcode()); + } + } + + private static void appendInvokeInstruction(StringBuilder out, Invoke invoke, int index) { + String owner = JavascriptNameUtil.sanitizeClassName(invoke.getOwner()); + String methodId = JavascriptNameUtil.methodIdentifier(invoke.getOwner(), invoke.getName(), invoke.getDesc()); + List args = JavascriptNameUtil.argumentTypes(invoke.getDesc()); + boolean hasReturn = invoke.getDesc().charAt(invoke.getDesc().length() - 1) != 'V'; + int argCount = args.size(); + switch (invoke.getOpcode()) { + case Opcodes.INVOKESTATIC: + case Opcodes.INVOKESPECIAL: + break; + case Opcodes.INVOKEVIRTUAL: + case Opcodes.INVOKEINTERFACE: + break; + default: + throw new IllegalArgumentException("Unsupported invoke opcode " + invoke.getOpcode()); + } + + if (invoke.getOpcode() == Opcodes.INVOKEVIRTUAL || invoke.getOpcode() == Opcodes.INVOKEINTERFACE) { + out.append(" {\n"); + appendInvocationArgumentBindings(out, argCount, " ", "stack.pop()"); + out.append(" const __target = stack.pop();\n"); + out.append(" const __method = ((jvm.classes[__target.__class] && jvm.classes[__target.__class].methods) ? jvm.classes[__target.__class].methods[\"").append(methodId) + .append("\"] : null) || jvm.resolveVirtual(__target.__class, \"").append(methodId).append("\");\n"); + if (hasReturn) { + out.append(" const __result = yield* __method("); + appendInvocationArguments(out, true, argCount); + out.append(");\n"); + out.append(" stack.push(__result);\n"); + } else { + out.append(" yield* __method("); + appendInvocationArguments(out, true, argCount); + out.append(");\n"); + } + out.append(" pc = ").append(index + 1).append("; break;\n"); + out.append(" }\n"); + return; + } + + out.append(" {\n"); + appendInvocationArgumentBindings(out, argCount, " ", "stack.pop()"); + if (invoke.getOpcode() != Opcodes.INVOKESTATIC) { + out.append(" const __target = stack.pop();\n"); + } + if (hasReturn) { + out.append(" const __result = yield* ").append(methodId).append("("); + } else { + out.append(" yield* ").append(methodId).append("("); + } + appendInvocationArguments(out, invoke.getOpcode() != Opcodes.INVOKESTATIC, argCount); + out.append(");\n"); + if (hasReturn) { + out.append(" stack.push(__result);\n"); + } + out.append(" pc = ").append(index + 1).append("; break;\n"); + out.append(" }\n"); + } + + private static void appendInvocationArguments(StringBuilder out, boolean includeTarget, int argCount) { + boolean first = true; + if (includeTarget) { + out.append("__target"); + first = false; + } + for (int i = 0; i < argCount; i++) { + if (!first) { + out.append(", "); + } + first = false; + out.append("__arg").append(i); + } + } + + private static void appendInvocationArgumentBindings(StringBuilder out, int argCount, String indent, String sourceExpression) { + for (int i = argCount - 1; i >= 0; i--) { + out.append(indent).append("const __arg").append(i).append(" = ").append(sourceExpression).append(";\n"); + } + } +} diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptNameUtil.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptNameUtil.java new file mode 100644 index 0000000000..a79d3c6556 --- /dev/null +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptNameUtil.java @@ -0,0 +1,162 @@ +package com.codename1.tools.translator; + +import java.util.ArrayList; +import java.util.List; + +final class JavascriptNameUtil { + private static final String SYMBOL_PREFIX = "cn1_"; + + private JavascriptNameUtil() { + } + + static String sanitizeClassName(String owner) { + return owner.replace('/', '_').replace('$', '_').replace('.', '_'); + } + + static String runtimeTypeName(String typeName) { + if (typeName == null || typeName.length() == 0) { + return typeName; + } + if (typeName.charAt(0) != '[') { + return sanitizeClassName(typeName); + } + int dimensions = 0; + while (dimensions < typeName.length() && typeName.charAt(dimensions) == '[') { + dimensions++; + } + String componentType; + char kind = typeName.charAt(dimensions); + if (kind == 'L') { + componentType = sanitizeClassName(typeName.substring(dimensions + 1, typeName.length() - 1)); + } else { + componentType = primitiveArrayComponent(kind); + } + StringBuilder out = new StringBuilder(componentType); + for (int i = 0; i < dimensions; i++) { + out.append("[]"); + } + return out.toString(); + } + + private static String primitiveArrayComponent(char kind) { + switch (kind) { + case 'Z': + return "JAVA_BOOLEAN"; + case 'C': + return "JAVA_CHAR"; + case 'F': + return "JAVA_FLOAT"; + case 'D': + return "JAVA_DOUBLE"; + case 'B': + return "JAVA_BYTE"; + case 'S': + return "JAVA_SHORT"; + case 'I': + return "JAVA_INT"; + case 'J': + return "JAVA_LONG"; + default: + return sanitizeClassName(String.valueOf(kind)); + } + } + + static String methodIdentifier(String owner, String name, String desc) { + StringBuilder b = new StringBuilder(); + b.append(SYMBOL_PREFIX).append(identifierPart(sanitizeClassName(owner))).append("_"); + if ("".equals(name)) { + b.append("__INIT__"); + } else if ("".equals(name)) { + b.append("__CLINIT__"); + } else { + b.append(identifierPart(name)); + } + BytecodeMethod.appendMethodSignatureSuffixFromDesc(desc, b, new ArrayList()); + return b.toString(); + } + + static String fieldProperty(String owner, String name) { + return SYMBOL_PREFIX + identifierPart(sanitizeClassName(owner)) + "_" + identifierPart(name); + } + + static String identifierPart(String value) { + StringBuilder out = new StringBuilder(value.length() + 8); + for (int i = 0; i < value.length(); i++) { + char ch = value.charAt(i); + if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_') { + out.append(ch); + } else { + out.append('_'); + } + } + if (out.length() == 0) { + out.append("value"); + } + return out.toString(); + } + + static String escapeJs(String value) { + StringBuilder out = new StringBuilder(value.length() + 16); + for (int i = 0; i < value.length(); i++) { + char ch = value.charAt(i); + switch (ch) { + case '\\': + out.append("\\\\"); + break; + case '"': + out.append("\\\""); + break; + case '\n': + out.append("\\n"); + break; + case '\r': + out.append("\\r"); + break; + case '\t': + out.append("\\t"); + break; + default: + if (ch < 32 || ch > 126) { + String hex = Integer.toHexString(ch); + out.append("\\u"); + for (int j = hex.length(); j < 4; j++) { + out.append('0'); + } + out.append(hex); + } else { + out.append(ch); + } + } + } + return out.toString(); + } + + static String defaultValue(String desc) { + if (desc == null || desc.isEmpty()) { + return "null"; + } + if (desc.length() != 1) { + return "null"; + } + char type = desc.charAt(0); + switch (type) { + case 'Z': + case 'C': + case 'F': + case 'D': + case 'B': + case 'S': + case 'I': + case 'J': + return "0"; + default: + return "null"; + } + } + + static List argumentTypes(String desc) { + List arguments = new ArrayList(); + BytecodeMethod.appendMethodSignatureSuffixFromDesc(desc, new StringBuilder(), arguments); + return arguments; + } +} diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptNativeRegistry.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptNativeRegistry.java new file mode 100644 index 0000000000..a3add069a3 --- /dev/null +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptNativeRegistry.java @@ -0,0 +1,161 @@ +package com.codename1.tools.translator; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +final class JavascriptNativeRegistry { + enum NativeCategory { + RUNTIME_IMPLEMENTED, + HOST_HOOK, + UNSUPPORTED, + UNCATEGORIZED + } + + private static final Set RUNTIME_IMPLEMENTED = new HashSet(Arrays.asList( + "cn1_java_io_InputStreamReader_bytesToChars_byte_1ARRAY_int_int_java_lang_String_R_char_1ARRAY", + "cn1_java_io_NSLogOutputStream_write_byte_1ARRAY_int_int", + "cn1_java_lang_Class_getNameImpl_R_java_lang_String", + "cn1_java_lang_Character_toLowerCase_char_R_char", + "cn1_java_lang_Character_toLowerCase_int_R_int", + "cn1_java_lang_Class_forNameImpl_java_lang_String_R_java_lang_Class", + "cn1_java_lang_Class_getComponentType_R_java_lang_Class", + "cn1_java_lang_Class_getName_R_java_lang_String", + "cn1_java_lang_Class_hashCode_R_int", + "cn1_java_lang_Class_isAnnotation_R_boolean", + "cn1_java_lang_Class_isAnonymousClass_R_boolean", + "cn1_java_lang_Class_isArray_R_boolean", + "cn1_java_lang_Class_isAssignableFrom_java_lang_Class_R_boolean", + "cn1_java_lang_Class_isEnum_R_boolean", + "cn1_java_lang_Class_isInstance_java_lang_Object_R_boolean", + "cn1_java_lang_Class_isInterface_R_boolean", + "cn1_java_lang_Class_isPrimitive_R_boolean", + "cn1_java_lang_Class_isSynthetic_R_boolean", + "cn1_java_lang_Class_newInstanceImpl_R_java_lang_Object", + "cn1_java_lang_Double_doubleToLongBits_double_R_long", + "cn1_java_lang_Double_longBitsToDouble_long_R_double", + "cn1_java_lang_Double_toStringImpl_double_boolean_R_java_lang_String", + "cn1_java_lang_Enum_valueOf_java_lang_Class_java_lang_String_R_java_lang_Enum", + "cn1_java_lang_Float_floatToIntBits_float_R_int", + "cn1_java_lang_Float_intBitsToFloat_int_R_float", + "cn1_java_lang_Float_toStringImpl_float_boolean_R_java_lang_String", + "cn1_java_lang_Integer_toString_int_R_java_lang_String", + "cn1_java_lang_Integer_toString_int_int_R_java_lang_String", + "cn1_java_lang_Long_toString_long_int_R_java_lang_String", + "cn1_java_lang_Math_abs_double_R_double", + "cn1_java_lang_Math_abs_float_R_float", + "cn1_java_lang_Math_abs_int_R_int", + "cn1_java_lang_Math_abs_long_R_long", + "cn1_java_lang_Math_atan_double_R_double", + "cn1_java_lang_Math_ceil_double_R_double", + "cn1_java_lang_Math_cos_double_R_double", + "cn1_java_lang_Math_floor_double_R_double", + "cn1_java_lang_Math_max_double_double_R_double", + "cn1_java_lang_Math_max_float_float_R_float", + "cn1_java_lang_Math_max_int_int_R_int", + "cn1_java_lang_Math_max_long_long_R_long", + "cn1_java_lang_Math_min_double_double_R_double", + "cn1_java_lang_Math_min_float_float_R_float", + "cn1_java_lang_Math_min_int_int_R_int", + "cn1_java_lang_Math_min_long_long_R_long", + "cn1_java_lang_Math_pow_double_double_R_double", + "cn1_java_lang_Math_sin_double_R_double", + "cn1_java_lang_Math_sqrt_double_R_double", + "cn1_java_lang_Math_tan_double_R_double", + "cn1_java_lang_Object_getClassImpl_R_java_lang_Class", + "cn1_java_lang_Object_hashCode_R_int", + "cn1_java_lang_Object_notify", + "cn1_java_lang_Object_notifyAll", + "cn1_java_lang_Object_toString_R_java_lang_String", + "cn1_java_lang_Object_wait_long_int", + "cn1_java_lang_Runtime_freeMemoryImpl_R_long", + "cn1_java_lang_Runtime_totalMemoryImpl_R_long", + "cn1_java_lang_StringBuilder_append_char_R_java_lang_StringBuilder", + "cn1_java_lang_StringBuilder_append_java_lang_Object_R_java_lang_StringBuilder", + "cn1_java_lang_StringBuilder_append_java_lang_String_R_java_lang_StringBuilder", + "cn1_java_lang_StringBuilder_charAt_int_R_char", + "cn1_java_lang_StringBuilder_getChars_int_int_char_1ARRAY_int", + "cn1_java_lang_String_bytesToChars_byte_1ARRAY_int_int_java_lang_String_R_char_1ARRAY", + "cn1_java_lang_String_charAt_int_R_char", + "cn1_java_lang_String_charsToBytes_char_1ARRAY_char_1ARRAY_R_byte_1ARRAY", + "cn1_java_lang_String_equalsIgnoreCase_java_lang_String_R_boolean", + "cn1_java_lang_String_equals_java_lang_Object_R_boolean", + "cn1_java_lang_String_format_java_lang_String_java_lang_Object_1ARRAY_R_java_lang_String", + "cn1_java_lang_String_getChars_int_int_char_1ARRAY_int", + "cn1_java_lang_String_hashCode_R_int", + "cn1_java_lang_String_indexOf_int_int_R_int", + "cn1_java_lang_String_releaseNSString_long", + "cn1_java_lang_String_toLowerCase_R_java_lang_String", + "cn1_java_lang_String_toString_R_java_lang_String", + "cn1_java_lang_String_toUpperCase_R_java_lang_String", + "cn1_java_lang_StringToReal_parseDblImpl_java_lang_String_int_R_double", + "cn1_java_lang_System_arraycopy_java_lang_Object_int_java_lang_Object_int_int", + "cn1_java_lang_System_currentTimeMillis_R_long", + "cn1_java_lang_System_exit_int", + "cn1_java_lang_System_gcLight", + "cn1_java_lang_System_gcMarkSweep", + "cn1_java_lang_System_identityHashCode_java_lang_Object_R_int", + "cn1_java_lang_System_isHighFrequencyGC_R_boolean", + "cn1_java_lang_Thread_currentThread_R_java_lang_Thread", + "cn1_java_lang_Thread_getNativeThreadId_R_long", + "cn1_java_lang_Thread_interrupt0", + "cn1_java_lang_Thread_isInterrupted_boolean_R_boolean", + "cn1_java_lang_Thread_releaseThreadNativeResources_long", + "cn1_java_lang_Thread_setPriorityImpl_int", + "cn1_java_lang_Thread_sleep_long", + "cn1_java_lang_Thread_start", + "cn1_java_lang_Throwable_fillInStack", + "cn1_java_lang_Throwable_getStack_R_java_lang_String", + "cn1_java_lang_reflect_Array_newInstanceImpl_java_lang_Class_int_R_java_lang_Object", + "cn1_java_text_DateFormat_format_java_util_Date_java_lang_StringBuffer_R_java_lang_String", + "cn1_java_util_HashMap_areEqualKeys_java_lang_Object_java_lang_Object_R_boolean", + "cn1_java_util_HashMap_findNonNullKeyEntry_java_lang_Object_int_int_R_java_util_HashMap_Entry", + "cn1_java_util_Locale_getOSLanguage_R_java_lang_String", + "cn1_java_util_TimeZone_getTimezoneId_R_java_lang_String", + "cn1_java_util_TimeZone_getTimezoneOffset_java_lang_String_int_int_int_int_R_int", + "cn1_java_util_TimeZone_getTimezoneRawOffset_java_lang_String_R_int", + "cn1_java_util_TimeZone_isTimezoneDST_java_lang_String_long_R_boolean", + "cn1_com_codename1_impl_platform_js_VMHost_getLastEventCode_R_int", + "cn1_com_codename1_impl_platform_js_VMHost_pollEventCode_R_int" + )); + + private static final Set HOST_HOOK_PREFIXES = new HashSet(Arrays.asList( + "cn1_com_codename1_impl_platform_js_VMHost_" + )); + + private JavascriptNativeRegistry() { + } + + static boolean hasRuntimeImplementation(String symbol) { + return RUNTIME_IMPLEMENTED.contains(symbol); + } + + static boolean isHostHook(String symbol) { + for (String prefix : HOST_HOOK_PREFIXES) { + if (symbol.startsWith(prefix)) { + return true; + } + } + return false; + } + + static NativeCategory categoryFor(String symbol) { + if (hasRuntimeImplementation(symbol)) { + return NativeCategory.RUNTIME_IMPLEMENTED; + } + if (isHostHook(symbol)) { + return NativeCategory.HOST_HOOK; + } + if (unsupportedReason(symbol) != null) { + return NativeCategory.UNSUPPORTED; + } + return NativeCategory.UNCATEGORIZED; + } + + static String unsupportedReason(String symbol) { + if (symbol.startsWith("cn1_java_io_File_")) { + return "java.io.File native filesystem access is not supported in javascript backend"; + } + return null; + } +} diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java index ab9203e555..591aa2bff1 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java @@ -60,6 +60,7 @@ public static void cleanup() { classes.clear(); dependencyGraph.clear(); BytecodeMethod.setDependencyGraph(null); + ByteCodeClass.cleanup(); LabelInstruction.cleanup(); } public static void parse(File sourceFile) throws Exception { @@ -67,7 +68,10 @@ public static void parse(File sourceFile) throws Exception { System.out.println("Parsing: " + sourceFile.getAbsolutePath()); } BytecodeMethod.setDependencyGraph(dependencyGraph); - ClassReader r = new ClassReader(Files.newInputStream(sourceFile.toPath())); + ClassReader r; + try (InputStream in = Files.newInputStream(sourceFile.toPath())) { + r = new ClassReader(in); + } Parser p = new Parser(); p.clsName = r.getClassName().replace('/', '_').replace('$', '_'); @@ -437,16 +441,20 @@ public static void writeOutput(File outputDirectory) throws Exception { } } - generateClassAndMethodIndexHeader(outputDirectory); + if (ByteCodeTranslator.output == ByteCodeTranslator.OutputType.OUTPUT_TYPE_JAVASCRIPT) { + JavascriptBundleWriter.write(outputDirectory, classes); + } else { + generateClassAndMethodIndexHeader(outputDirectory); - boolean concatenate = "true".equals(System.getProperty("concatenateFiles", "false")); - ConcatenatingFileOutputStream cos = concatenate ? new ConcatenatingFileOutputStream(outputDirectory) : null; + boolean concatenate = "true".equals(System.getProperty("concatenateFiles", "false")); + ConcatenatingFileOutputStream cos = concatenate ? new ConcatenatingFileOutputStream(outputDirectory) : null; - for(ByteCodeClass bc : classes) { - file = bc.getClsName(); - writeFile(bc, outputDirectory, cos); + for(ByteCodeClass bc : classes) { + file = bc.getClsName(); + writeFile(bc, outputDirectory, cos); + } + if (cos != null) cos.realClose(); } - if (cos != null) cos.realClose(); } catch(Throwable t) { System.out.println("Error while working with the class: " + file); t.printStackTrace(); @@ -630,6 +638,9 @@ private static void writeFile(ByteCodeClass cls, File outputDir, ConcatenatingFi if(ByteCodeTranslator.output == ByteCodeTranslator.OutputType.OUTPUT_TYPE_CSHARP) { outMain.write(cls.generateCSharpCode().getBytes(StandardCharsets.UTF_8)); outMain.close(); + } else if (ByteCodeTranslator.output == ByteCodeTranslator.OutputType.OUTPUT_TYPE_JAVASCRIPT) { + outMain.write(cls.generateJavascriptCode(classes).getBytes(StandardCharsets.UTF_8)); + outMain.close(); } else { outMain.write(cls.generateCCode(classes).getBytes(StandardCharsets.UTF_8)); outMain.close(); diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/Field.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/Field.java index f795d4a165..e490fb1e6f 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/Field.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/Field.java @@ -50,6 +50,18 @@ public boolean isObject() { char c = desc.charAt(0); return c == '[' || c == 'L'; } + + public String getOwner() { + return owner; + } + + public String getFieldName() { + return name; + } + + public String getDesc() { + return desc; + } @Override public void addDependencies(List dependencyList) { diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/IInc.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/IInc.java index 01515d6f58..e3894106ae 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/IInc.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/IInc.java @@ -43,6 +43,10 @@ public int getVar() { return var; } + public int getAmount() { + return num; + } + @Override public void appendInstruction(StringBuilder b) { if(getMethod() != null && getMethod().isBarebone()) { diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/Invoke.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/Invoke.java index 51d64aeec5..5e10c18876 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/Invoke.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/Invoke.java @@ -54,19 +54,19 @@ public Invoke(int opcode, String owner, String name, String desc, boolean itf) { this.itf = itf; } - String getOwner() { + public String getOwner() { return owner; } - String getName() { + public String getName() { return name; } - String getDesc() { + public String getDesc() { return desc; } - boolean isItf() { + public boolean isItf() { return itf; } diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/Jump.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/Jump.java index e85997b362..cc0594d406 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/Jump.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/Jump.java @@ -123,7 +123,7 @@ public void appendInstruction(StringBuilder b, List instructions) { } } - Label getLabel() { + public Label getLabel() { return label; } diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/LabelInstruction.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/LabelInstruction.java index 01234a13aa..296e36f64a 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/LabelInstruction.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/LabelInstruction.java @@ -68,6 +68,10 @@ public LabelInstruction(org.objectweb.asm.Label parent) { super(-1); this.parent = parent; } + + public Label getLabel() { + return parent; + } public static int getLabelCatchDepth(Label l, List inst) { Integer i = labelCatchDepth.get(l); diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/MultiArray.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/MultiArray.java index 15bb64cc4d..ee56ad1874 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/MultiArray.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/MultiArray.java @@ -41,6 +41,14 @@ public MultiArray(String desc, int dims) { this.desc = desc; this.dims = dims; } + + public String getDesc() { + return desc; + } + + public int getDimensionsToAllocate() { + return dims; + } @Override public void addDependencies(List dependencyList) { diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/SwitchInstruction.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/SwitchInstruction.java index a11b9d0ee1..711b8d2086 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/SwitchInstruction.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/SwitchInstruction.java @@ -82,4 +82,16 @@ public void appendInstruction(StringBuilder b, List instructions) { b.append(" }\n"); } + public Label getDefaultLabel() { + return dflt; + } + + public int[] getKeys() { + return keys; + } + + public Label[] getLabels() { + return labels; + } + } diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/TryCatch.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/TryCatch.java index a84b55d05b..c85f92cfa0 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/TryCatch.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/TryCatch.java @@ -69,6 +69,22 @@ public void addDependencies(List dependencyList) { public static boolean isTryCatchInMethod() { return hasTryCatch; } + + public Label getStart() { + return start; + } + + public Label getEnd() { + return end; + } + + public Label getHandler() { + return handler; + } + + public String getType() { + return type; + } @Override public void appendInstruction(StringBuilder b, List instructions) { diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/TypeInstruction.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/TypeInstruction.java index 2176473103..9547ae014c 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/TypeInstruction.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/bytecodes/TypeInstruction.java @@ -39,6 +39,14 @@ public TypeInstruction(int opcode, String type) { this.type = type; } + public String getTypeName() { + return type; + } + + public String getActualType() { + return actualType; + } + @Override public void addDependencies(List dependencyList) { String t = type.replace('.', '_').replace('/', '_').replace('$', '_'); diff --git a/vm/ByteCodeTranslator/src/javascript/index.html b/vm/ByteCodeTranslator/src/javascript/index.html new file mode 100644 index 0000000000..f7119e8936 --- /dev/null +++ b/vm/ByteCodeTranslator/src/javascript/index.html @@ -0,0 +1,23 @@ + + + + +ParparVM JS + + + + + diff --git a/vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js b/vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js new file mode 100644 index 0000000000..471918bd51 --- /dev/null +++ b/vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js @@ -0,0 +1,1221 @@ +(function(global) { +const CN1_STRING_VALUE = "cn1_java_lang_String_value"; +const CN1_STRING_OFFSET = "cn1_java_lang_String_offset"; +const CN1_STRING_COUNT = "cn1_java_lang_String_count"; +const CN1_STRING_HASH = "cn1_java_lang_String_hashCode"; +const CN1_SB_VALUE = "cn1_java_lang_StringBuilder_value"; +const CN1_SB_COUNT = "cn1_java_lang_StringBuilder_count"; +const CN1_THREAD_ALIVE = "cn1_java_lang_Thread_alive"; +const CN1_THREAD_NAME = "cn1_java_lang_Thread_name"; +const CN1_THREAD_NATIVE_ID = "cn1_java_lang_Thread_nativeThreadId"; +const CN1_THREAD_TARGET = "cn1_java_lang_Thread_target"; +const CN1_THROWABLE_MESSAGE = "cn1_java_lang_Throwable_message"; +const CN1_THROWABLE_STACK = "cn1_java_lang_Throwable_stack"; +const CN1_DATE_VALUE = "cn1_java_util_Date_date"; +const CN1_DATEFORMAT_DATE_STYLE = "cn1_java_text_DateFormat_dateStyle"; +const CN1_DATEFORMAT_TIME_STYLE = "cn1_java_text_DateFormat_timeStyle"; +const CN1_STRINGBUFFER_INTERNAL = "cn1_java_lang_StringBuffer_internal"; +const CN1_ENUM_NAME = "cn1_java_lang_Enum_name"; +const CN1_HASHMAP_ELEMENT_DATA = "cn1_java_util_HashMap_elementData"; +const CN1_HASHMAP_ENTRY_NEXT = "cn1_java_util_HashMap_Entry_next"; +const CN1_HASHMAP_ENTRY_KEY = "cn1_java_util_MapEntry_key"; +const VM_PROTOCOL_VERSION = 1; +const VM_PROTOCOL = Object.freeze({ + version: VM_PROTOCOL_VERSION, + messages: Object.freeze({ + START: "start", + EVENT: "event", + UI_EVENT: "ui-event", + TIMER_WAKE: "timer-wake", + HOST_CALL: "host-call", + HOST_CALLBACK: "host-callback", + PROTOCOL_INFO: "protocol-info", + PROTOCOL: "protocol", + LOG: "log", + RESULT: "result", + ERROR: "error" + }) +}); +const PRIMITIVE_INFO = { + JAVA_BOOLEAN: { javaName: "boolean", descriptor: "Z" }, + JAVA_CHAR: { javaName: "char", descriptor: "C" }, + JAVA_FLOAT: { javaName: "float", descriptor: "F" }, + JAVA_DOUBLE: { javaName: "double", descriptor: "D" }, + JAVA_BYTE: { javaName: "byte", descriptor: "B" }, + JAVA_SHORT: { javaName: "short", descriptor: "S" }, + JAVA_INT: { javaName: "int", descriptor: "I" }, + JAVA_LONG: { javaName: "long", descriptor: "J" } +}; +const jvm = { + classes: {}, + literalStrings: Object.create(null), + methodTailCache: Object.create(null), + nextIdentity: 1, + nextThreadId: 1, + nextHostCallId: 1, + currentThread: null, + runnable: [], + threads: [], + pendingHostCalls: Object.create(null), + eventQueue: [], + mainClass: null, + mainMethod: null, + protocol: VM_PROTOCOL, + defineClass(def) { + def.staticFields = def.staticFields || {}; + def.instanceFields = def.instanceFields || []; + def.assignableTo = def.assignableTo || {}; + def.methods = def.methods || {}; + def.classObject = { + __class: "java_lang_Class", + __monitor: this.createMonitor(), + __className: def.name, + __isClassObject: true, + __classDef: def, + cn1_staticFields: def.staticFields + }; + this.classes[def.name] = def; + }, + addVirtualMethod(className, methodId, fn) { + this.classes[className].methods[methodId] = fn; + }, + setMain(className, methodName) { + this.mainClass = className; + this.mainMethod = methodName; + }, + createMonitor() { + return { owner: null, count: 0, waiters: [], entrants: [] }; + }, + getClassObject(className) { + const cls = this.classes[className]; + if (!cls) { + throw new Error("Unknown class " + className); + } + return cls.classObject; + }, + ensureClassInitialized(className) { + const cls = this.classes[className]; + if (!cls) { + throw new Error("Unknown class " + className); + } + if (cls.initialized || cls.initializing) { + return; + } + cls.initializing = true; + if (cls.baseClass) { + this.ensureClassInitialized(cls.baseClass); + } + cls.initialized = true; + if (cls.clinit) { + const gen = cls.clinit(); + let step = gen.next(); + while (!step.done) { + if (step.value && (step.value.op === "sleep" || step.value.op === "wait")) { + throw new Error("Blocking static initializers are not supported in javascript backend"); + } + step = gen.next(); + } + } + cls.initializing = false; + }, + newObject(className) { + this.ensureClassInitialized(className); + const classDef = this.classes[className]; + const obj = { __class: className, __classDef: classDef, __id: this.nextIdentity++, __monitor: this.createMonitor() }; + this.initInstanceFields(obj, className); + return obj; + }, + initInstanceFields(obj, className) { + const cls = this.classes[className]; + if (!cls) { + return; + } + if (cls.baseClass) { + this.initInstanceFields(obj, cls.baseClass); + } + for (const field of cls.instanceFields) { + obj[field.prop || (field.owner + "_" + field.name)] = null; + if (field.desc && field.desc.length && field.desc.charAt(0) !== "L" && field.desc.charAt(0) !== "[") { + obj[field.prop || (field.owner + "_" + field.name)] = 0; + } + } + }, + newArray(size, componentClass, dimensions) { + size = size | 0; + if (size < 0) { + throw new Error("Negative array size"); + } + const array = new Array(size); + for (let i = 0; i < size; i++) { + array[i] = null; + } + array.__class = this.arrayClassName(componentClass, dimensions); + array.__classDef = this.getArrayClass(componentClass, dimensions); + array.__dimensions = dimensions; + array.__array = true; + array.__monitor = this.createMonitor(); + return array; + }, + newMultiArray(sizes, componentClass, dimensions, depth) { + const level = depth || 0; + const size = sizes[level] | 0; + const array = this.newArray(size, componentClass, dimensions - level); + if ((dimensions - level) <= 1) { + return array; + } + const nextSize = sizes[level + 1]; + if (nextSize == null || nextSize < 0) { + return array; + } + for (let i = 0; i < size; i++) { + array[i] = this.newMultiArray(sizes, componentClass, dimensions, level + 1); + } + return array; + }, + arrayClassName(componentClass, dimensions) { + let name = componentClass; + for (let i = 0; i < dimensions; i++) { + name += "[]"; + } + return name; + }, + getArrayClass(componentClass, dimensions) { + const className = this.arrayClassName(componentClass, dimensions); + let cls = this.classes[className]; + if (!cls) { + cls = { + name: className, + baseClass: "java_lang_Object", + componentClass: componentClass, + dimensions: dimensions, + assignableTo: this.arrayAssignableTo(componentClass, dimensions), + staticFields: {} + }; + cls.classObject = { + __class: "java_lang_Class", + __monitor: this.createMonitor(), + __className: className, + __isClassObject: true, + __classDef: cls, + cn1_staticFields: cls.staticFields + }; + this.classes[className] = cls; + } + return cls; + }, + arrayAssignableTo(componentClass, dimensions) { + const out = { java_lang_Object: true }; + out[this.arrayClassName(componentClass, dimensions)] = true; + if (this.isPrimitiveComponent(componentClass)) { + return out; + } + const componentClassDef = this.classes[componentClass]; + if (!componentClassDef || !componentClassDef.assignableTo) { + for (let i = 1; i <= dimensions; i++) { + out[this.arrayClassName("java_lang_Object", i)] = true; + } + return out; + } + const componentTargets = Object.keys(componentClassDef.assignableTo); + for (let i = 0; i < componentTargets.length; i++) { + const target = componentTargets[i]; + if (target === "java_lang_Object") { + for (let depth = 1; depth <= dimensions; depth++) { + out[this.arrayClassName(target, depth)] = true; + } + } else { + out[this.arrayClassName(target, dimensions)] = true; + } + } + return out; + }, + isPrimitiveComponent(componentClass) { + return componentClass.indexOf("JAVA_") === 0; + }, + resolveVirtual(className, methodId) { + const tail = this.methodTail(methodId); + let current = className; + while (current) { + const cls = this.classes[current]; + if (cls && cls.methods) { + if (cls.methods[methodId]) { + return cls.methods[methodId]; + } + if (tail) { + const remappedId = "cn1_" + current + tail; + if (cls.methods[remappedId]) { + return cls.methods[remappedId]; + } + } + } + current = cls ? cls.baseClass : null; + } + throw new Error("Missing virtual method " + methodId + " on " + className); + }, + methodTail(methodId) { + let cached = this.methodTailCache[methodId]; + if (cached !== undefined) { + return cached; + } + let bestPrefix = null; + for (const className in this.classes) { + const prefix = "cn1_" + className; + if (methodId.indexOf(prefix) === 0 && (bestPrefix == null || prefix.length > bestPrefix.length)) { + bestPrefix = prefix; + } + } + if (bestPrefix != null) { + cached = methodId.substring(bestPrefix.length); + this.methodTailCache[methodId] = cached; + return cached; + } + this.methodTailCache[methodId] = null; + return null; + }, + instanceOf(obj, className) { + return !!(obj && obj.__classDef && obj.__classDef.assignableTo && obj.__classDef.assignableTo[className]); + }, + findExceptionHandler(entries, pc, error) { + if (!entries || !entries.length) { + return null; + } + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + if (pc < entry.start || pc >= entry.end) { + continue; + } + if (entry.type == null) { + return entry; + } + if (this.instanceOf(error, entry.type)) { + return entry; + } + } + return null; + }, + createStringLiteral(value) { + if (!this.literalStrings[value]) { + const chars = this.newArray(value.length, "JAVA_CHAR", 1); + for (let i = 0; i < value.length; i++) { + chars[i] = value.charCodeAt(i); + } + const str = this.newObject("java_lang_String"); + str[CN1_STRING_VALUE] = chars; + str[CN1_STRING_OFFSET] = 0; + str[CN1_STRING_COUNT] = value.length; + str[CN1_STRING_HASH] = 0; + str.__nativeString = value; + this.literalStrings[value] = str; + } + return this.literalStrings[value]; + }, + toNativeString(value) { + if (value == null) { + return "null"; + } + if (typeof value === "string") { + return value; + } + if (value.__nativeString != null) { + return value.__nativeString; + } + if (value.__class === "java_lang_String") { + const data = value[CN1_STRING_VALUE]; + const offset = value[CN1_STRING_OFFSET] | 0; + const count = value[CN1_STRING_COUNT] | 0; + let out = ""; + for (let i = 0; i < count; i++) { + out += String.fromCharCode(data[offset + i] | 0); + } + value.__nativeString = out; + return out; + } + return "" + value; + }, + log(message) { + global.postMessage({ type: this.protocol.messages.LOG, message: message }); + }, + finish(result) { + global.postMessage({ type: this.protocol.messages.RESULT, result: result }); + }, + fail(error) { + global.postMessage({ type: this.protocol.messages.ERROR, message: "" + error, stack: error && error.stack ? error.stack : null }); + }, + invokeHostNative(symbol, args) { + return { op: this.protocol.messages.HOST_CALL, id: this.nextHostCallId++, symbol: symbol, args: args || [] }; + }, + resolveHostCall(id, success, value, error) { + const pending = this.pendingHostCalls[id]; + if (!pending) { + return false; + } + delete this.pendingHostCalls[id]; + if (success) { + this.enqueue(pending.thread, value); + } else { + pending.thread.waiting = null; + pending.thread.resumeError = error instanceof Error ? error : new Error(error == null ? "Host callback failed" : String(error)); + this.runnable.push(pending.thread); + this.drain(); + } + return true; + }, + spawn(threadObject, generator) { + const thread = { id: this.nextThreadId++, object: threadObject, generator: generator, waiting: null, interrupted: false, done: false }; + this.threads.push(thread); + this.enqueue(thread); + return thread; + }, + enqueue(thread, value) { + thread.waiting = null; + thread.resumeValue = value; + this.runnable.push(thread); + this.drain(); + }, + drain() { + if (this.draining) { + return; + } + this.draining = true; + try { + while (this.runnable.length) { + const thread = this.runnable.shift(); + if (thread.done) { + continue; + } + this.currentThread = thread; + let result; + if (thread.resumeError) { + const resumeError = thread.resumeError; + thread.resumeError = null; + result = thread.generator.throw(resumeError); + } else { + result = thread.generator.next(thread.resumeValue); + } + thread.resumeValue = undefined; + if (result.done) { + thread.done = true; + if (thread.object) { + thread.object[CN1_THREAD_ALIVE] = 0; + this.notifyAll(thread.object); + } + continue; + } + this.handleYield(thread, result.value); + } + } catch (err) { + this.fail(err); + } finally { + this.currentThread = null; + this.draining = false; + } + }, + handleYield(thread, yielded) { + if (!yielded || !yielded.op) { + this.enqueue(thread, yielded); + return; + } + if (yielded.op === "sleep") { + const timer = setTimeout(() => this.enqueue(thread), Math.max(0, yielded.millis | 0)); + thread.waiting = { op: "sleep", timer: timer }; + return; + } + if (yielded.op === "wait") { + const waiter = { thread: thread, monitor: yielded.monitor, reentryCount: yielded.reentryCount }; + yielded.monitor.__monitor.waiters.push(waiter); + if (yielded.timeout > 0) { + waiter.timer = setTimeout(() => this.resumeWaiter(waiter), yielded.timeout); + } + thread.waiting = { op: "wait", waiter: waiter }; + return; + } + if (yielded.op === this.protocol.messages.HOST_CALL) { + thread.waiting = { op: this.protocol.messages.HOST_CALL, id: yielded.id }; + this.pendingHostCalls[yielded.id] = { thread: thread }; + global.postMessage({ type: this.protocol.messages.HOST_CALL, id: yielded.id, symbol: yielded.symbol, args: yielded.args || [] }); + return; + } + throw new Error("Unsupported yield op " + yielded.op); + }, + resumeWaiter(waiter) { + const list = waiter.monitor.__monitor.waiters; + const index = list.indexOf(waiter); + if (index >= 0) { + list.splice(index, 1); + } + const monitor = waiter.monitor.__monitor || (waiter.monitor.__monitor = this.createMonitor()); + if (monitor.owner == null || monitor.owner === waiter.thread.id) { + monitor.owner = waiter.thread.id; + monitor.count = waiter.reentryCount; + this.enqueue(waiter.thread, waiter.resumeValue); + return; + } + monitor.entrants.push(waiter); + }, + monitorEnter(thread, obj) { + const monitor = obj.__monitor || (obj.__monitor = this.createMonitor()); + if (monitor.owner == null || monitor.owner === thread.id) { + monitor.owner = thread.id; + monitor.count++; + return; + } + throw new Error("Blocking monitor acquisition is not yet supported in javascript backend"); + }, + monitorExit(thread, obj) { + const monitor = obj.__monitor || (obj.__monitor = this.createMonitor()); + if (monitor.owner !== thread.id) { + throw new Error("IllegalMonitorStateException"); + } + monitor.count--; + if (monitor.count <= 0) { + monitor.count = 0; + monitor.owner = null; + if (monitor.entrants.length) { + const next = monitor.entrants.shift(); + monitor.owner = next.thread.id; + monitor.count = next.reentryCount; + this.enqueue(next.thread, next.resumeValue); + } + } + }, + waitOn(thread, obj, timeout) { + const monitor = obj.__monitor || (obj.__monitor = this.createMonitor()); + if (monitor.owner !== thread.id) { + throw new Error("IllegalMonitorStateException"); + } + const reentryCount = monitor.count; + monitor.owner = null; + monitor.count = 0; + return { op: "wait", monitor: obj, timeout: timeout | 0, reentryCount: reentryCount }; + }, + notifyOne(obj) { + const monitor = obj.__monitor || (obj.__monitor = this.createMonitor()); + const waiter = monitor.waiters.shift(); + if (!waiter) { + return; + } + if (waiter.timer) { + clearTimeout(waiter.timer); + } + this.resumeWaiter(waiter); + }, + notifyAll(obj) { + const monitor = obj.__monitor || (obj.__monitor = this.createMonitor()); + const waiters = monitor.waiters.splice(0, monitor.waiters.length); + for (const waiter of waiters) { + if (waiter.timer) { + clearTimeout(waiter.timer); + } + this.resumeWaiter(waiter); + } + }, + findThreadByObject(obj) { + for (let i = 0; i < this.threads.length; i++) { + if (this.threads[i].object === obj) { + return this.threads[i]; + } + } + return null; + }, + interruptThread(threadObject) { + if (!threadObject) { + return; + } + threadObject.__interrupted = 1; + const thread = this.findThreadByObject(threadObject); + if (!thread || !thread.waiting) { + return; + } + if (thread.waiting.op === "sleep") { + clearTimeout(thread.waiting.timer); + this.enqueue(thread, { interrupted: true }); + return; + } + if (thread.waiting.op === "wait") { + const waiter = thread.waiting.waiter; + if (waiter.timer) { + clearTimeout(waiter.timer); + } + const monitor = waiter.monitor.__monitor || (waiter.monitor.__monitor = this.createMonitor()); + const index = monitor.waiters.indexOf(waiter); + if (index >= 0) { + monitor.waiters.splice(index, 1); + } + if (monitor.owner == null || monitor.owner === thread.id) { + monitor.owner = thread.id; + monitor.count = waiter.reentryCount; + this.enqueue(thread, { interrupted: true }); + } else { + waiter.resumeValue = { interrupted: true }; + monitor.entrants.push(waiter); + } + } + }, + createException(className) { + const ex = this.newObject(className); + const ctor = global["cn1_" + className + "___INIT__"]; + return { object: ex, ctor: ctor }; + }, + start() { + if (!this.mainClass || !this.mainMethod) { + throw new Error("No main class configured for javascript backend"); + } + const mainArgs = this.newArray(0, "java_lang_String", 1); + const mainThreadObject = this.newObject("java_lang_Thread"); + mainThreadObject[CN1_THREAD_ALIVE] = 1; + mainThreadObject[CN1_THREAD_NAME] = this.createStringLiteral("main"); + const mainThread = this.spawn(mainThreadObject, global[this.mainMethod](mainArgs)); + this.currentThread = mainThread; + }, + describeProtocol() { + return { + type: this.protocol.messages.PROTOCOL, + version: this.protocol.version, + messages: this.protocol.messages + }; + }, + handleMessage(message) { + if (!message || !message.type) { + return false; + } + if (message.type === this.protocol.messages.PROTOCOL_INFO) { + global.postMessage(this.describeProtocol()); + return true; + } + if (message.type === this.protocol.messages.HOST_CALLBACK) { + return this.resolveHostCall(message.id, !message.error, message.value, message.errorMessage || message.error); + } + if (message.type === this.protocol.messages.TIMER_WAKE) { + this.drain(); + return true; + } + if (message.type === this.protocol.messages.EVENT || message.type === this.protocol.messages.UI_EVENT) { + this.lastEvent = message; + this.eventQueue.push(message); + return true; + } + return false; + } +}; + +global.jvm = jvm; +function createJavaString(value) { + value = value == null ? "" : String(value); + return jvm.createStringLiteral(value); +} +function javaClassName(className) { + if (PRIMITIVE_INFO[className]) { + return PRIMITIVE_INFO[className].javaName; + } + return String(className || "").replace(/_/g, "."); +} +function descriptorClassName(className) { + if (PRIMITIVE_INFO[className]) { + return PRIMITIVE_INFO[className].descriptor; + } + if (String(className).endsWith("[]")) { + const dims = className.match(/\[\]/g).length; + const component = className.substring(0, className.length - (dims * 2)); + let out = ""; + for (let i = 0; i < dims; i++) { + out += "["; + } + if (PRIMITIVE_INFO[component]) { + return out + PRIMITIVE_INFO[component].descriptor; + } + return out + "L" + javaClassName(component) + ";"; + } + return javaClassName(className); +} +function ensurePrimitiveClass(componentClass) { + let cls = jvm.classes[componentClass]; + if (!cls) { + cls = { + name: componentClass, + isPrimitive: true, + assignableTo: {}, + staticFields: {}, + methods: {} + }; + cls.assignableTo[componentClass] = true; + cls.classObject = { + __class: "java_lang_Class", + __monitor: jvm.createMonitor(), + __className: componentClass, + __isClassObject: true, + __classDef: cls, + cn1_staticFields: cls.staticFields + }; + jvm.classes[componentClass] = cls; + } + return cls.classObject; +} +function classObjectForName(name) { + if (PRIMITIVE_INFO[name]) { + return ensurePrimitiveClass(name); + } + return jvm.getClassObject(name); +} +function runtimeTypeFromJavaName(name) { + if (name == null) { + return null; + } + if (PRIMITIVE_INFO["JAVA_" + String(name).toUpperCase()]) { + return "JAVA_" + String(name).toUpperCase(); + } + if (name.charAt(0) === "[") { + let dims = 0; + while (name.charAt(dims) === "[") { + dims++; + } + const kind = name.charAt(dims); + let component; + if (kind === "L") { + component = name.substring(dims + 1, name.length - 1).replace(/[.$/]/g, "_"); + } else { + for (const primitiveName in PRIMITIVE_INFO) { + if (PRIMITIVE_INFO[primitiveName].descriptor === kind) { + component = primitiveName; + break; + } + } + } + let out = component; + for (let i = 0; i < dims; i++) { + out += "[]"; + } + return out; + } + return name.replace(/[.$/]/g, "_"); +} +function createArrayFromNativeString(value) { + const chars = jvm.newArray(value.length, "JAVA_CHAR", 1); + for (let i = 0; i < value.length; i++) { + chars[i] = value.charCodeAt(i); + } + return chars; +} +function nativeStringFromCharArray(chars) { + let out = ""; + for (let i = 0; i < chars.length; i++) { + out += String.fromCharCode(chars[i] | 0); + } + return out; +} +function* runtimeToNativeString(value) { + if (value == null || typeof value === "string" || value.__nativeString != null || value.__class === "java_lang_String") { + return jvm.toNativeString(value); + } + if (value && value.__class) { + const toStringMethod = jvm.resolveVirtual(value.__class, "cn1_java_lang_Object_toString_R_java_lang_String"); + return jvm.toNativeString(yield* toStringMethod(value)); + } + return String(value); +} +function sbEnsureCapacity(sb, size) { + let data = sb[CN1_SB_VALUE]; + if (!data) { + data = jvm.newArray(Math.max(16, size), "JAVA_CHAR", 1); + sb[CN1_SB_VALUE] = data; + return data; + } + if (data.length >= size) { + return data; + } + const next = jvm.newArray(Math.max(size, (data.length * 2) + 2), "JAVA_CHAR", 1); + for (let i = 0; i < data.length; i++) { + next[i] = data[i]; + } + sb[CN1_SB_VALUE] = next; + return next; +} +function sbAppendNativeString(sb, value) { + value = value == null ? "null" : String(value); + const count = sb[CN1_SB_COUNT] | 0; + const data = sbEnsureCapacity(sb, count + value.length); + for (let i = 0; i < value.length; i++) { + data[count + i] = value.charCodeAt(i); + } + sb[CN1_SB_COUNT] = count + value.length; + return sb; +} +function intBitsFromFloat(value) { + const view = new DataView(new ArrayBuffer(4)); + view.setFloat32(0, value, false); + return view.getInt32(0, false); +} +function floatFromIntBits(bits) { + const view = new DataView(new ArrayBuffer(4)); + view.setInt32(0, bits | 0, false); + return view.getFloat32(0, false); +} +function longBitsFromDouble(value) { + const view = new DataView(new ArrayBuffer(8)); + view.setFloat64(0, value, false); + const hi = view.getUint32(0, false); + const lo = view.getUint32(4, false); + return (hi * 4294967296) + lo; +} +function doubleFromLongBits(bits) { + const hi = Math.floor(bits / 4294967296); + const lo = bits >>> 0; + const view = new DataView(new ArrayBuffer(8)); + view.setUint32(0, hi >>> 0, false); + view.setUint32(4, lo, false); + return view.getFloat64(0, false); +} +function defaultTimeZoneId() { + if (typeof Intl !== "undefined" && Intl.DateTimeFormat) { + const options = Intl.DateTimeFormat().resolvedOptions(); + if (options && options.timeZone) { + return options.timeZone; + } + } + return "GMT"; +} +function normalizeTimeZoneId(name) { + const value = name == null ? "" : jvm.toNativeString(name); + return value ? value : defaultTimeZoneId(); +} +function timezoneDateParts(timeZone, millis) { + const format = new Intl.DateTimeFormat("en-US", { + timeZone: timeZone, + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hourCycle: "h23" + }); + const parts = format.formatToParts(new Date(millis)); + const out = {}; + for (let i = 0; i < parts.length; i++) { + if (parts[i].type !== "literal") { + out[parts[i].type] = parts[i].value; + } + } + return out; +} +function timezoneOffsetMillis(timeZone, millis) { + if (timeZone === "GMT" || typeof Intl === "undefined" || !Intl.DateTimeFormat) { + return 0; + } + const parts = timezoneDateParts(timeZone, millis); + const utcMillis = Date.UTC( + parseInt(parts.year, 10), + parseInt(parts.month, 10) - 1, + parseInt(parts.day, 10), + parseInt(parts.hour, 10), + parseInt(parts.minute, 10), + parseInt(parts.second, 10), + 0); + return utcMillis - millis; +} +function timezoneRawOffsetMillis(timeZone) { + if (timeZone === "GMT") { + return 0; + } + const year = new Date().getUTCFullYear(); + const jan = timezoneOffsetMillis(timeZone, Date.UTC(year, 0, 1, 12, 0, 0, 0)); + const jul = timezoneOffsetMillis(timeZone, Date.UTC(year, 6, 1, 12, 0, 0, 0)); + return Math.min(jan, jul); +} +function formatStyleOptions(style, type) { + if (style < 0) { + return null; + } + switch (style | 0) { + case 0: + return type === "date" ? { weekday: "long", year: "numeric", month: "long", day: "numeric" } + : { hour: "numeric", minute: "2-digit", second: "2-digit", timeZoneName: "short" }; + case 1: + return type === "date" ? { year: "numeric", month: "long", day: "numeric" } + : { hour: "numeric", minute: "2-digit", second: "2-digit", timeZoneName: "short" }; + case 2: + return type === "date" ? { year: "numeric", month: "short", day: "numeric" } + : { hour: "numeric", minute: "2-digit", second: "2-digit" }; + default: + return type === "date" ? { year: "2-digit", month: "numeric", day: "numeric" } + : { hour: "numeric", minute: "2-digit" }; + } +} +function formatJavaDate(dateFormat, dateObject) { + const millis = dateObject == null ? Date.now() : Number(dateObject[CN1_DATE_VALUE] || 0); + const date = new Date(millis); + const dateOptions = formatStyleOptions(dateFormat[CN1_DATEFORMAT_DATE_STYLE] | 0, "date"); + const timeOptions = formatStyleOptions(dateFormat[CN1_DATEFORMAT_TIME_STYLE] | 0, "time"); + const options = {}; + if (dateOptions) { + Object.assign(options, dateOptions); + } + if (timeOptions) { + Object.assign(options, timeOptions); + } + if (!dateOptions && !timeOptions) { + options.year = "numeric"; + options.month = "numeric"; + options.day = "numeric"; + } + return date.toLocaleString(undefined, options); +} +function* throwInterruptedException() { + if (jvm.currentThread && jvm.currentThread.object) { + jvm.currentThread.object.__interrupted = 0; + } + const ex = jvm.createException("java_lang_InterruptedException"); + if (typeof ex.ctor === "function") { + yield* ex.ctor(ex.object); + } + throw ex.object; +} +function bindNative(names, fn) { + for (let i = 0; i < names.length; i++) { + global[names[i]] = jvm[names[i]] = fn; + } + return fn; +} +bindNative(["cn1_java_lang_Object_wait_long_int", "cn1_java_lang_Object_wait___long_int"], function*(__cn1ThisObject, timeout, nanos) { + const resumed = yield jvm.waitOn(jvm.currentThread, __cn1ThisObject, timeout || 0); + if (resumed && resumed.interrupted) { + yield* throwInterruptedException(); + } + return null; +}); +bindNative(["cn1_java_lang_Object_notify", "cn1_java_lang_Object_notify__"], function*(__cn1ThisObject) { jvm.notifyOne(__cn1ThisObject); return null; }); +bindNative(["cn1_java_lang_Object_notifyAll", "cn1_java_lang_Object_notifyAll__"], function*(__cn1ThisObject) { jvm.notifyAll(__cn1ThisObject); return null; }); +bindNative(["cn1_java_lang_Object_hashCode_R_int", "cn1_java_lang_Object_hashCode___R_int"], function*(__cn1ThisObject) { return __cn1ThisObject == null ? 0 : (__cn1ThisObject.__id | 0); }); +bindNative(["cn1_java_lang_Object_getClassImpl_R_java_lang_Class", "cn1_java_lang_Object_getClassImpl___R_java_lang_Class"], function*(__cn1ThisObject) { return __cn1ThisObject && __cn1ThisObject.__classDef ? __cn1ThisObject.__classDef.classObject : jvm.getClassObject(__cn1ThisObject.__class); }); +bindNative(["cn1_java_lang_Object_toString_R_java_lang_String"], function*(__cn1ThisObject) { + if (__cn1ThisObject == null) { + return createJavaString("null"); + } + return createJavaString(javaClassName(__cn1ThisObject.__class) + "@" + ((__cn1ThisObject.__id | 0).toString(16))); +}); +bindNative(["cn1_java_lang_Thread_currentThread_R_java_lang_Thread", "cn1_java_lang_Thread_currentThread___R_java_lang_Thread"], function*() { return jvm.currentThread ? jvm.currentThread.object : null; }); +bindNative(["cn1_java_lang_Thread_sleep_long", "cn1_java_lang_Thread_sleep___long"], function*(millis) { + const resumed = yield { op: "sleep", millis: millis || 0 }; + if (resumed && resumed.interrupted) { + yield* throwInterruptedException(); + } + return null; +}); +bindNative(["cn1_java_lang_Thread_setPriorityImpl_int", "cn1_java_lang_Thread_setPriorityImpl___int"], function*() { return null; }); +bindNative(["cn1_java_lang_Thread_interrupt0", "cn1_java_lang_Thread_interrupt0__"], function*(__cn1ThisObject) { jvm.interruptThread(__cn1ThisObject); return null; }); +bindNative(["cn1_java_lang_Thread_isInterrupted_boolean_R_boolean", "cn1_java_lang_Thread_isInterrupted___boolean_R_boolean"], function*(__cn1ThisObject, clearInterrupted) { const value = __cn1ThisObject && __cn1ThisObject.__interrupted ? 1 : 0; if (clearInterrupted && __cn1ThisObject) __cn1ThisObject.__interrupted = 0; return value; }); +bindNative(["cn1_java_lang_Thread_getNativeThreadId_R_long", "cn1_java_lang_Thread_getNativeThreadId___R_long"], function*() { return jvm.currentThread ? jvm.currentThread.id : 0; }); +bindNative(["cn1_java_lang_Thread_releaseThreadNativeResources_long", "cn1_java_lang_Thread_releaseThreadNativeResources___long"], function*() { return null; }); +bindNative(["cn1_java_lang_Thread_start", "cn1_java_lang_Thread_start__"], function*(__cn1ThisObject) { + const tid = jvm.nextThreadId; + __cn1ThisObject[CN1_THREAD_ALIVE] = 1; + __cn1ThisObject[CN1_THREAD_NATIVE_ID] = tid; + jvm.classes["java_lang_Thread"].staticFields["activeThreads"] = ((jvm.classes["java_lang_Thread"].staticFields["activeThreads"] | 0) + 1) | 0; + const target = __cn1ThisObject[CN1_THREAD_TARGET] || __cn1ThisObject; + const generator = (function*() { + try { + const runMethod = jvm.resolveVirtual(target.__class, "cn1_java_lang_Runnable_run"); + yield* runMethod(target); + } catch (err) { + jvm.fail(err); + } finally { + jvm.classes["java_lang_Thread"].staticFields["activeThreads"] = ((jvm.classes["java_lang_Thread"].staticFields["activeThreads"] | 0) - 1) | 0; + } + })(); + jvm.spawn(__cn1ThisObject, generator); + return null; +}); +bindNative(["cn1_java_lang_System_currentTimeMillis_R_long", "cn1_java_lang_System_currentTimeMillis___R_long"], function*() { return Date.now(); }); +bindNative(["cn1_java_lang_System_identityHashCode_java_lang_Object_R_int", "cn1_java_lang_System_identityHashCode___java_lang_Object_R_int"], function*(obj) { return obj == null ? 0 : (obj.__id | 0); }); +bindNative(["cn1_java_lang_System_arraycopy_java_lang_Object_int_java_lang_Object_int_int", "cn1_java_lang_System_arraycopy___java_lang_Object_int_java_lang_Object_int_int"], function*(src, srcOffset, dst, dstOffset, length) { + for (let i = 0; i < length; i++) dst[dstOffset + i] = src[srcOffset + i]; + return null; +}); +bindNative(["cn1_java_lang_System_gcLight", "cn1_java_lang_System_gcLight__"], function*() { return null; }); +bindNative(["cn1_java_lang_System_gcMarkSweep", "cn1_java_lang_System_gcMarkSweep__"], function*() { return null; }); +bindNative(["cn1_java_lang_System_isHighFrequencyGC_R_boolean", "cn1_java_lang_System_isHighFrequencyGC___R_boolean"], function*() { return 0; }); +bindNative(["cn1_java_lang_System_exit_int", "cn1_java_lang_System_exit___int"], function*(status) { jvm.finish(status); return null; }); +bindNative(["cn1_java_lang_Runtime_totalMemoryImpl_R_long"], function*() { return 67108864; }); +bindNative(["cn1_java_lang_Runtime_freeMemoryImpl_R_long"], function*() { return 33554432; }); +bindNative(["cn1_java_lang_Throwable_fillInStack"], function*(__cn1ThisObject) { __cn1ThisObject[CN1_THROWABLE_STACK] = createJavaString(new Error().stack || ""); return null; }); +bindNative(["cn1_java_lang_Throwable_getStack_R_java_lang_String"], function*(__cn1ThisObject) { return __cn1ThisObject[CN1_THROWABLE_STACK] || createJavaString(""); }); +bindNative(["cn1_java_lang_Math_abs_double_R_double"], function*(v) { return Math.abs(v); }); +bindNative(["cn1_java_lang_Math_abs_float_R_float"], function*(v) { return Math.abs(v); }); +bindNative(["cn1_java_lang_Math_abs_int_R_int"], function*(v) { return Math.abs(v | 0); }); +bindNative(["cn1_java_lang_Math_abs_long_R_long"], function*(v) { return Math.abs(v); }); +bindNative(["cn1_java_lang_Math_ceil_double_R_double"], function*(v) { return Math.ceil(v); }); +bindNative(["cn1_java_lang_Math_floor_double_R_double"], function*(v) { return Math.floor(v); }); +bindNative(["cn1_java_lang_Math_max_double_double_R_double"], function*(a, b) { return Math.max(a, b); }); +bindNative(["cn1_java_lang_Math_max_float_float_R_float"], function*(a, b) { return Math.max(a, b); }); +bindNative(["cn1_java_lang_Math_max_int_int_R_int"], function*(a, b) { return Math.max(a | 0, b | 0); }); +bindNative(["cn1_java_lang_Math_max_long_long_R_long"], function*(a, b) { return Math.max(a, b); }); +bindNative(["cn1_java_lang_Math_min_double_double_R_double"], function*(a, b) { return Math.min(a, b); }); +bindNative(["cn1_java_lang_Math_min_float_float_R_float"], function*(a, b) { return Math.min(a, b); }); +bindNative(["cn1_java_lang_Math_min_int_int_R_int"], function*(a, b) { return Math.min(a | 0, b | 0); }); +bindNative(["cn1_java_lang_Math_min_long_long_R_long"], function*(a, b) { return Math.min(a, b); }); +bindNative(["cn1_java_lang_Math_pow_double_double_R_double"], function*(a, b) { return Math.pow(a, b); }); +bindNative(["cn1_java_lang_Math_cos_double_R_double"], function*(v) { return Math.cos(v); }); +bindNative(["cn1_java_lang_Math_sin_double_R_double"], function*(v) { return Math.sin(v); }); +bindNative(["cn1_java_lang_Math_sqrt_double_R_double"], function*(v) { return Math.sqrt(v); }); +bindNative(["cn1_java_lang_Math_tan_double_R_double"], function*(v) { return Math.tan(v); }); +bindNative(["cn1_java_lang_Math_atan_double_R_double"], function*(v) { return Math.atan(v); }); +bindNative(["cn1_java_lang_Integer_toString_int_R_java_lang_String"], function*(v) { return createJavaString(String(v | 0)); }); +bindNative(["cn1_java_lang_Integer_toString_int_int_R_java_lang_String"], function*(v, radix) { return createJavaString((v | 0).toString((radix | 0) || 10)); }); +bindNative(["cn1_java_lang_Long_toString_long_int_R_java_lang_String"], function*(v, radix) { return createJavaString(Math.trunc(v).toString((radix | 0) || 10)); }); +bindNative(["cn1_java_lang_Character_toLowerCase_char_R_char"], function*(ch) { return String.fromCharCode(ch | 0).toLowerCase().charCodeAt(0) | 0; }); +bindNative(["cn1_java_lang_Character_toLowerCase_int_R_int"], function*(ch) { return String.fromCharCode(ch | 0).toLowerCase().charCodeAt(0) | 0; }); +bindNative(["cn1_java_lang_Float_floatToIntBits_float_R_int"], function*(v) { return intBitsFromFloat(v); }); +bindNative(["cn1_java_lang_Float_intBitsToFloat_int_R_float"], function*(bits) { return floatFromIntBits(bits); }); +bindNative(["cn1_java_lang_Float_toStringImpl_float_boolean_R_java_lang_String"], function*(v) { return createJavaString(String(v)); }); +bindNative(["cn1_java_lang_Double_doubleToLongBits_double_R_long"], function*(v) { return longBitsFromDouble(v); }); +bindNative(["cn1_java_lang_Double_longBitsToDouble_long_R_double"], function*(bits) { return doubleFromLongBits(bits); }); +bindNative(["cn1_java_lang_Double_toStringImpl_double_boolean_R_java_lang_String"], function*(v) { return createJavaString(String(v)); }); +bindNative(["cn1_java_lang_StringBuilder_append_char_R_java_lang_StringBuilder"], function*(__cn1ThisObject, ch) { return sbAppendNativeString(__cn1ThisObject, String.fromCharCode(ch | 0)); }); +bindNative(["cn1_java_lang_StringBuilder_append_java_lang_Object_R_java_lang_StringBuilder"], function*(__cn1ThisObject, obj) { return sbAppendNativeString(__cn1ThisObject, jvm.toNativeString(obj)); }); +bindNative(["cn1_java_lang_StringBuilder_append_java_lang_String_R_java_lang_StringBuilder"], function*(__cn1ThisObject, str) { return sbAppendNativeString(__cn1ThisObject, jvm.toNativeString(str)); }); +bindNative(["cn1_java_lang_StringBuilder_charAt_int_R_char"], function*(__cn1ThisObject, index) { return (__cn1ThisObject[CN1_SB_VALUE][index | 0] || 0) | 0; }); +bindNative(["cn1_java_lang_StringBuilder_getChars_int_int_char_1ARRAY_int"], function*(__cn1ThisObject, start, end, dst, dstStart) { + const value = __cn1ThisObject[CN1_SB_VALUE]; + for (let i = start | 0; i < (end | 0); i++) { + dst[(dstStart | 0) + i - (start | 0)] = value[i] | 0; + } + return null; +}); +bindNative(["cn1_java_lang_String_charAt_int_R_char"], function*(__cn1ThisObject, index) { return jvm.toNativeString(__cn1ThisObject).charCodeAt(index | 0) | 0; }); +bindNative(["cn1_java_lang_String_equals_java_lang_Object_R_boolean"], function*(__cn1ThisObject, obj) { + return (obj != null && obj.__class === "java_lang_String" && jvm.toNativeString(__cn1ThisObject) === jvm.toNativeString(obj)) ? 1 : 0; +}); +bindNative(["cn1_java_lang_String_equalsIgnoreCase_java_lang_String_R_boolean"], function*(__cn1ThisObject, other) { + return (other != null && jvm.toNativeString(__cn1ThisObject).toLowerCase() === jvm.toNativeString(other).toLowerCase()) ? 1 : 0; +}); +bindNative(["cn1_java_lang_String_getChars_int_int_char_1ARRAY_int"], function*(__cn1ThisObject, start, end, dst, dstStart) { + const value = jvm.toNativeString(__cn1ThisObject); + for (let i = start | 0; i < (end | 0); i++) { + dst[(dstStart | 0) + i - (start | 0)] = value.charCodeAt(i) | 0; + } + return null; +}); +bindNative(["cn1_java_lang_String_hashCode_R_int"], function*(__cn1ThisObject) { + let hash = __cn1ThisObject[CN1_STRING_HASH] | 0; + if (hash !== 0) { + return hash; + } + const value = jvm.toNativeString(__cn1ThisObject); + for (let i = 0; i < value.length; i++) { + hash = (((hash * 31) | 0) + value.charCodeAt(i)) | 0; + } + __cn1ThisObject[CN1_STRING_HASH] = hash; + return hash; +}); +bindNative(["cn1_java_lang_String_indexOf_int_int_R_int"], function*(__cn1ThisObject, ch, fromIndex) { return jvm.toNativeString(__cn1ThisObject).indexOf(String.fromCharCode(ch | 0), fromIndex | 0); }); +bindNative(["cn1_java_lang_String_toLowerCase_R_java_lang_String"], function*(__cn1ThisObject) { return createJavaString(jvm.toNativeString(__cn1ThisObject).toLowerCase()); }); +bindNative(["cn1_java_lang_String_toString_R_java_lang_String"], function*(__cn1ThisObject) { return __cn1ThisObject; }); +bindNative(["cn1_java_lang_String_toUpperCase_R_java_lang_String"], function*(__cn1ThisObject) { return createJavaString(jvm.toNativeString(__cn1ThisObject).toUpperCase()); }); +bindNative(["cn1_java_lang_String_releaseNSString_long"], function*() { return null; }); +bindNative(["cn1_java_lang_String_bytesToChars_byte_1ARRAY_int_int_java_lang_String_R_char_1ARRAY"], function*(bytes, off, len, encoding) { + const slice = bytes.slice(off | 0, (off | 0) + (len | 0)); + const array = Uint8Array.from(slice, function(v) { return v & 0xff; }); + let text = ""; + if (typeof TextDecoder !== "undefined") { + try { + text = new TextDecoder((encoding ? jvm.toNativeString(encoding) : "utf-8")).decode(array); + } catch (err) { + text = new TextDecoder("utf-8").decode(array); + } + } else { + for (let i = 0; i < array.length; i++) { + text += String.fromCharCode(array[i]); + } + } + return createArrayFromNativeString(text); +}); +bindNative(["cn1_java_io_InputStreamReader_bytesToChars_byte_1ARRAY_int_int_java_lang_String_R_char_1ARRAY"], function*(bytes, off, len, encoding) { + return yield* cn1_java_lang_String_bytesToChars_byte_1ARRAY_int_int_java_lang_String_R_char_1ARRAY(bytes, off, len, encoding); +}); +bindNative(["cn1_java_lang_String_charsToBytes_char_1ARRAY_char_1ARRAY_R_byte_1ARRAY"], function*(chars) { + let text = ""; + for (let i = 0; i < chars.length; i++) { + text += String.fromCharCode(chars[i] | 0); + } + let encoded; + if (typeof TextEncoder !== "undefined") { + encoded = new TextEncoder().encode(text); + } else { + encoded = Uint8Array.from(text.split("").map(function(ch) { return ch.charCodeAt(0) & 0xff; })); + } + const out = jvm.newArray(encoded.length, "JAVA_BYTE", 1); + for (let i = 0; i < encoded.length; i++) { + out[i] = encoded[i]; + } + return out; +}); +bindNative(["cn1_java_lang_String_format_java_lang_String_java_lang_Object_1ARRAY_R_java_lang_String"], function*(format, args) { + let index = 0; + const values = []; + if (args && args.__array) { + for (let i = 0; i < args.length; i++) { + values.push(yield* runtimeToNativeString(args[i])); + } + } + const result = jvm.toNativeString(format).replace(/%[%sdifc]/g, function(token) { + if (token === "%%") { + return "%"; + } + const value = values[index++]; + if (token === "%c") { + return String.fromCharCode(value | 0); + } + return value; + }); + return createJavaString(result); +}); +bindNative(["cn1_java_lang_StringToReal_parseDblImpl_java_lang_String_int_R_double"], function*(value, exponentIndex) { + const text = jvm.toNativeString(value); + const parsed = Number(text); + return isNaN(parsed) ? 0 : parsed; +}); +bindNative(["cn1_java_lang_Enum_valueOf_java_lang_Class_java_lang_String_R_java_lang_Enum"], function*(enumType, name) { + if (!enumType || !enumType.__classDef) { + return null; + } + jvm.ensureClassInitialized(enumType.__classDef.name); + const matchName = jvm.toNativeString(name); + if (enumType.cn1_staticFields) { + const staticFieldNames = Object.keys(enumType.cn1_staticFields); + for (let i = 0; i < staticFieldNames.length; i++) { + const candidate = enumType.cn1_staticFields[staticFieldNames[i]]; + if (candidate && candidate.__array) { + for (let j = 0; j < candidate.length; j++) { + const arrayEntry = candidate[j]; + if (arrayEntry != null + && arrayEntry.__class === enumType.__classDef.name + && jvm.toNativeString(arrayEntry[CN1_ENUM_NAME]) === matchName) { + return arrayEntry; + } + } + } + if (candidate && candidate.__class === enumType.__classDef.name + && jvm.toNativeString(candidate[CN1_ENUM_NAME]) === matchName) { + return candidate; + } + } + } + return null; +}); +bindNative(["cn1_java_lang_Class_forNameImpl_java_lang_String_R_java_lang_Class"], function*(className) { + const runtimeName = runtimeTypeFromJavaName(jvm.toNativeString(className)); + const cls = jvm.classes[runtimeName]; + return cls ? cls.classObject : null; +}); +bindNative(["cn1_java_lang_Class_getNameImpl_R_java_lang_String"], function*(__cn1ThisObject) { + return createJavaString(javaClassName(__cn1ThisObject.__classDef.name)); +}); +bindNative(["cn1_java_lang_Class_getName_R_java_lang_String"], function*(__cn1ThisObject) { return createJavaString(descriptorClassName(__cn1ThisObject.__classDef.name)); }); +bindNative(["cn1_java_lang_Class_isArray_R_boolean"], function*(__cn1ThisObject) { return __cn1ThisObject.__classDef && __cn1ThisObject.__classDef.name.indexOf("[]") > -1 ? 1 : 0; }); +bindNative(["cn1_java_lang_Class_isAssignableFrom_java_lang_Class_R_boolean"], function*(__cn1ThisObject, cls) { return cls && cls.__classDef && cls.__classDef.assignableTo[__cn1ThisObject.__classDef.name] ? 1 : 0; }); +bindNative(["cn1_java_lang_Class_isInstance_java_lang_Object_R_boolean"], function*(__cn1ThisObject, obj) { return jvm.instanceOf(obj, __cn1ThisObject.__classDef.name) ? 1 : 0; }); +bindNative(["cn1_java_lang_Class_isInterface_R_boolean"], function*(__cn1ThisObject) { return __cn1ThisObject.__classDef && __cn1ThisObject.__classDef.isInterface ? 1 : 0; }); +bindNative(["cn1_java_lang_Class_newInstanceImpl_R_java_lang_Object"], function*(__cn1ThisObject) { + const def = __cn1ThisObject.__classDef; + if (!def || def.isInterface || def.isAbstract || def.isPrimitive || def.name.indexOf("[]") > -1) { + return null; + } + const obj = jvm.newObject(def.name); + const ctor = global["cn1_" + def.name + "___INIT__"]; + if (typeof ctor === "function") { + yield* ctor(obj); + } + return obj; +}); +bindNative(["cn1_java_lang_Class_isAnnotation_R_boolean"], function*() { return 0; }); +bindNative(["cn1_java_lang_Class_isEnum_R_boolean"], function*() { return 0; }); +bindNative(["cn1_java_lang_Class_isAnonymousClass_R_boolean"], function*() { return 0; }); +bindNative(["cn1_java_lang_Class_isSynthetic_R_boolean"], function*() { return 0; }); +bindNative(["cn1_java_lang_Class_hashCode_R_int"], function*(__cn1ThisObject) { return __cn1ThisObject && __cn1ThisObject.__classDef ? (__cn1ThisObject.__classDef.name.length | 0) : 0; }); +bindNative(["cn1_java_lang_Class_getComponentType_R_java_lang_Class"], function*(__cn1ThisObject) { + const def = __cn1ThisObject.__classDef; + if (!def || def.name.indexOf("[]") < 0) { + return null; + } + return classObjectForName(def.componentClass); +}); +bindNative(["cn1_java_lang_Class_isPrimitive_R_boolean"], function*(__cn1ThisObject) { return __cn1ThisObject.__classDef && __cn1ThisObject.__classDef.isPrimitive ? 1 : 0; }); +bindNative(["cn1_java_lang_reflect_Array_newInstanceImpl_java_lang_Class_int_R_java_lang_Object"], function*(componentClass, length) { + if (!componentClass || !componentClass.__classDef) { + return null; + } + return jvm.newArray(length | 0, componentClass.__classDef.name, 1); +}); +bindNative(["cn1_java_util_Locale_getOSLanguage_R_java_lang_String"], function*() { + let locale = null; + if (typeof navigator !== "undefined" && navigator.language) { + locale = navigator.language; + } else if (typeof Intl !== "undefined" && Intl.DateTimeFormat) { + locale = Intl.DateTimeFormat().resolvedOptions().locale; + } + return createJavaString(locale || "en-US"); +}); +bindNative(["cn1_java_util_TimeZone_getTimezoneId_R_java_lang_String"], function*() { + return createJavaString(defaultTimeZoneId()); +}); +bindNative(["cn1_java_util_TimeZone_getTimezoneOffset_java_lang_String_int_int_int_int_R_int"], function*(name, year, month, day, timeOfDayMillis) { + const tz = normalizeTimeZoneId(name); + const millis = Date.UTC((year | 0), ((month | 0) - 1), day | 0, 0, 0, 0, 0) + (timeOfDayMillis | 0); + return timezoneOffsetMillis(tz, millis); +}); +bindNative(["cn1_java_util_TimeZone_getTimezoneRawOffset_java_lang_String_R_int"], function*(name) { + return timezoneRawOffsetMillis(normalizeTimeZoneId(name)); +}); +bindNative(["cn1_java_util_TimeZone_isTimezoneDST_java_lang_String_long_R_boolean"], function*(name, millis) { + const tz = normalizeTimeZoneId(name); + return timezoneOffsetMillis(tz, millis) !== timezoneRawOffsetMillis(tz) ? 1 : 0; +}); +bindNative(["cn1_java_text_DateFormat_format_java_util_Date_java_lang_StringBuffer_R_java_lang_String"], function*(__cn1ThisObject, date, toAppendTo) { + const formatted = createJavaString(formatJavaDate(__cn1ThisObject, date)); + if (toAppendTo != null && toAppendTo[CN1_STRINGBUFFER_INTERNAL] != null) { + sbAppendNativeString(toAppendTo[CN1_STRINGBUFFER_INTERNAL], jvm.toNativeString(formatted)); + } + return formatted; +}); +bindNative(["cn1_java_util_HashMap_areEqualKeys_java_lang_Object_java_lang_Object_R_boolean"], function*(key1, key2) { + if (key1 === key2) { + return 1; + } + if (key1 == null || key2 == null) { + return 0; + } + const equalsMethod = jvm.resolveVirtual(key1.__class, "cn1_java_lang_Object_equals_java_lang_Object_R_boolean"); + return (yield* equalsMethod(key1, key2)) ? 1 : 0; +}); +bindNative(["cn1_java_util_HashMap_findNonNullKeyEntry_java_lang_Object_int_int_R_java_util_HashMap_Entry"], function*(__cn1ThisObject, key, index, keyHash) { + const buckets = __cn1ThisObject[CN1_HASHMAP_ELEMENT_DATA]; + let entry = buckets == null ? null : buckets[index | 0]; + while (entry != null) { + if (((entry.cn1_java_util_HashMap_Entry_origKeyHash | 0) === (keyHash | 0)) + && (yield* cn1_java_util_HashMap_areEqualKeys_java_lang_Object_java_lang_Object_R_boolean(key, entry[CN1_HASHMAP_ENTRY_KEY]))) { + return entry; + } + entry = entry[CN1_HASHMAP_ENTRY_NEXT]; + } + return null; +}); +bindNative(["cn1_java_io_NSLogOutputStream_write_byte_1ARRAY_int_int"], function*(__cn1ThisObject, bytes, off, len) { + const chars = yield* cn1_java_lang_String_bytesToChars_byte_1ARRAY_int_int_java_lang_String_R_char_1ARRAY(bytes, off, len, createJavaString("utf-8")); + jvm.log(nativeStringFromCharArray(chars)); + return null; +}); +bindNative(["cn1_com_codename1_impl_platform_js_VMHost_getLastEventCode_R_int", + "cn1_com_codename1_impl_platform_js_VMHost_getLastEventCode___R_int"], function*() { + if (!jvm.lastEvent || jvm.lastEvent.code == null) { + return -1; + } + return jvm.lastEvent.code | 0; +}); +bindNative(["cn1_com_codename1_impl_platform_js_VMHost_pollEventCode_R_int", + "cn1_com_codename1_impl_platform_js_VMHost_pollEventCode___R_int"], function*() { + if (!jvm.eventQueue.length) { + return -1; + } + const event = jvm.eventQueue.shift(); + return event && event.code != null ? (event.code | 0) : -1; +}); +})(self); diff --git a/vm/ByteCodeTranslator/src/javascript/worker.js b/vm/ByteCodeTranslator/src/javascript/worker.js new file mode 100644 index 0000000000..9ab7413530 --- /dev/null +++ b/vm/ByteCodeTranslator/src/javascript/worker.js @@ -0,0 +1,18 @@ +self.window = self; +self.global = self; +/*__IMPORTS__*/ +self.onmessage = function(event) { + if (!event || !event.data) { + return; + } + const protocol = jvm.protocol.messages; + if (event.data.type === protocol.START) { + jvm.start(); + } else if (event.data.type === protocol.PROTOCOL_INFO + || event.data.type === protocol.UI_EVENT + || event.data.type === protocol.EVENT + || event.data.type === protocol.HOST_CALLBACK + || event.data.type === protocol.TIMER_WAKE) { + jvm.handleMessage(event.data); + } +}; diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/CompilerHelper.java b/vm/tests/src/test/java/com/codename1/tools/translator/CompilerHelper.java index 54acf7424d..e8e78b65a1 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/CompilerHelper.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/CompilerHelper.java @@ -13,7 +13,9 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Properties; import java.util.TreeMap; +import java.util.stream.Stream; /** * Helper class to manage external JDK compilers. @@ -30,6 +32,9 @@ public class CompilerHelper { checkAndAddJdk("17", System.getenv("JDK_17_HOME")); checkAndAddJdk("21", System.getenv("JDK_21_HOME")); checkAndAddJdk("25", System.getenv("JDK_25_HOME")); + checkAndAddDetectedJdk(System.getenv("JAVA_HOME")); + + discoverLocalJdks(); // Fallback: If no env vars, assume current JVM is JDK 8 (or whatever is running) // This ensures tests pass locally or in environments not fully configured with all JDKs @@ -49,6 +54,94 @@ private static void checkAndAddJdk(String version, String path) { } } + private static void checkAndAddDetectedJdk(String path) { + if (path == null || path.isEmpty()) { + return; + } + Path jdkHome = normalizeJdkHome(Paths.get(path)); + String version = detectJdkVersion(jdkHome); + if (version != null) { + availableJdks.put(version, jdkHome); + } + } + + private static void discoverLocalJdks() { + Path userJdks = Paths.get(System.getProperty("user.home"), "Library", "Java", "JavaVirtualMachines"); + Path systemJdks = Paths.get("/Library", "Java", "JavaVirtualMachines"); + scanJdkDirectory(userJdks); + scanJdkDirectory(systemJdks); + } + + private static void scanJdkDirectory(Path root) { + if (!Files.isDirectory(root)) { + return; + } + try (Stream paths = Files.list(root)) { + paths.forEach(candidate -> { + String version = detectJdkVersion(candidate); + if (version != null) { + availableJdks.put(version, normalizeJdkHome(candidate)); + } + }); + } catch (IOException ignored) { + } + } + + private static Path normalizeJdkHome(Path path) { + if (path == null) { + return null; + } + Path contentsHome = path.resolve("Contents").resolve("Home"); + if (Files.exists(contentsHome.resolve("bin").resolve(executableName("javac")))) { + return contentsHome; + } + return path; + } + + private static String detectJdkVersion(Path path) { + Path jdkHome = normalizeJdkHome(path); + if (jdkHome == null || !Files.exists(jdkHome.resolve("bin").resolve(executableName("javac")))) { + return null; + } + + Path releaseFile = jdkHome.resolve("release"); + if (Files.isRegularFile(releaseFile)) { + Properties props = new Properties(); + try (InputStream in = Files.newInputStream(releaseFile)) { + props.load(in); + String version = props.getProperty("JAVA_VERSION"); + if (version != null) { + version = version.replace("\"", "").trim(); + int major = parseJavaMajor(version); + if (major > 0) { + return Integer.toString(major); + } + } + } catch (IOException ignored) { + } + } + + int major = parseJavaMajor(jdkHome.getFileName().toString()); + if (major > 0) { + return Integer.toString(major); + } + Path parent = jdkHome.getParent(); + if (parent != null) { + major = parseJavaMajor(parent.getFileName().toString()); + if (major > 0) { + return Integer.toString(major); + } + } + return null; + } + + private static String executableName(String base) { + if (System.getProperty("os.name").toLowerCase().contains("win")) { + return base + ".exe"; + } + return base; + } + public static List getAvailableCompilers(String targetVersion) { List compilers = new ArrayList<>(); @@ -275,9 +368,10 @@ public static void compileJavaAPI(Path outputDir, CompilerConfig config) throws Files.createDirectories(outputDir); Path javaApiRoot = Paths.get("..", "JavaAPI", "src").normalize().toAbsolutePath(); List sources = new ArrayList<>(); - Files.walk(javaApiRoot) - .filter(p -> p.toString().endsWith(".java")) - .forEach(p -> sources.add(p.toString())); + try (Stream paths = Files.walk(javaApiRoot)) { + paths.filter(p -> p.toString().endsWith(".java")) + .forEach(p -> sources.add(p.toString())); + } List args = new ArrayList<>(); @@ -308,18 +402,20 @@ public static void compileJavaAPI(Path outputDir, CompilerConfig config) throws } public static void copyDirectory(Path sourceDir, Path targetDir) throws IOException { - Files.walk(sourceDir).forEach(source -> { - try { - Path destination = targetDir.resolve(sourceDir.relativize(source)); - if (Files.isDirectory(source)) { - Files.createDirectories(destination); - } else { - Files.createDirectories(destination.getParent()); - Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING); + try (Stream paths = Files.walk(sourceDir)) { + paths.forEach(source -> { + try { + Path destination = targetDir.resolve(sourceDir.relativize(source)); + if (Files.isDirectory(source)) { + Files.createDirectories(destination); + } else { + Files.createDirectories(destination.getParent()); + Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING); + } + } catch (IOException e) { + throw new RuntimeException(e); } - } catch (IOException e) { - throw new RuntimeException(e); - } - }); + }); + } } } diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptCn1CoreCompletenessTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptCn1CoreCompletenessTest.java new file mode 100644 index 0000000000..54cd29c01d --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptCn1CoreCompletenessTest.java @@ -0,0 +1,147 @@ +package com.codename1.tools.translator; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class JavascriptCn1CoreCompletenessTest { + + @Test + void translatesMeaningfulCodenameOneCoreSliceWithoutUncategorizedNativeGaps() throws Exception { + Parser.cleanup(); + + CompilerHelper.CompilerConfig config = selectRepresentativeCompiler(); + assertTrue(CompilerHelper.isJavaApiCompatible(config), + "JDK " + config.jdkVersion + " must target matching bytecode level for JavaAPI"); + + Path sourceDir = Files.createTempDirectory("js-cn1-core-src"); + Path classesDir = Files.createTempDirectory("js-cn1-core-classes"); + Path javaApiDir = Files.createTempDirectory("js-cn1-core-javaapi"); + + Files.write(sourceDir.resolve("JsCodenameOneCoreSliceApp.java"), + JavascriptTargetIntegrationTest.loadFixture("JsCodenameOneCoreSliceApp.java").getBytes(StandardCharsets.UTF_8)); + + Path coreJar = findDependencyJar("codenameone-core"); + assertNotNull(coreJar, "codenameone-core dependency jar should be available in target/benchmark-dependencies"); + + CompilerHelper.compileJavaAPI(javaApiDir, config); + compileFixtureAgainstJavaApiAndCore(config, sourceDir, classesDir, javaApiDir, coreJar); + + CompilerHelper.copyDirectory(javaApiDir, classesDir); + unzipMatching(coreJar, classesDir, + "com/codename1/io/", + "com/codename1/util/", + "com/codename1/compat/java/", + "com/codename1/l10n/", + "com/codename1/ui/events/", + "com/codename1/xml/"); + + Path outputDir = Files.createTempDirectory("js-cn1-core-output"); + JavascriptTargetIntegrationTest.runJavascriptTranslator(classesDir, outputDir, "JsCodenameOneCoreSliceApp"); + + Path distDir = outputDir.resolve("dist").resolve("JsCodenameOneCoreSliceApp-js"); + assertTrue(Files.exists(distDir.resolve("translated_app.js")), + "Translator should emit translated JS for the Codename One core slice"); + + String translatedApp = new String(Files.readAllBytes(distDir.resolve("translated_app.js")), StandardCharsets.UTF_8); + assertTrue(translatedApp.contains("JsCodenameOneCoreSliceApp"), + "Translated bundle should contain the CN1 core slice app entrypoint"); + assertFalse(translatedApp.contains("Missing javascript native method "), + "CN1 core slice translation should not retain uncategorized javascript native fallback stubs"); + } + + private static CompilerHelper.CompilerConfig selectRepresentativeCompiler() { + String[] preferredTargets = new String[] {"11", "17", "21"}; + for (String target : preferredTargets) { + for (CompilerHelper.CompilerConfig config : CompilerHelper.getAvailableCompilers(target)) { + if (target.equals(config.targetVersion) && CompilerHelper.isJavaApiCompatible(config)) { + return config; + } + } + } + throw new AssertionError("No representative JDK 11+ compiler available for Codename One core slice translation"); + } + + private static void compileFixtureAgainstJavaApiAndCore(CompilerHelper.CompilerConfig config, Path sourceDir, + Path classesDir, Path javaApiDir, Path coreJar) throws Exception { + List sources = new ArrayList(); + try (Stream paths = Files.walk(sourceDir)) { + paths.filter(path -> path.toString().endsWith(".java")).forEach(path -> sources.add(path.toString())); + } + + List compileArgs = new ArrayList(); + compileArgs.add("-source"); + compileArgs.add(config.targetVersion); + compileArgs.add("-target"); + compileArgs.add(config.targetVersion); + compileArgs.add("-classpath"); + compileArgs.add(javaApiDir.toString() + java.io.File.pathSeparator + coreJar.toString()); + if (!CompilerHelper.useClasspath(config)) { + compileArgs.add("-bootclasspath"); + compileArgs.add(javaApiDir.toString()); + compileArgs.add("-Xlint:-options"); + } + compileArgs.add("-d"); + compileArgs.add(classesDir.toString()); + compileArgs.addAll(sources); + + int compileResult = CompilerHelper.compile(config.jdkHome, compileArgs); + assertEquals(0, compileResult, + "Compilation failed for Codename One core slice fixture with " + config + ": " + CompilerHelper.getLastErrorLog()); + } + + private static Path findDependencyJar(String namePart) throws IOException { + Path depsDir = Paths.get("target", "benchmark-dependencies"); + if (!Files.exists(depsDir)) { + return null; + } + try (Stream paths = Files.list(depsDir)) { + return paths.filter(path -> path.getFileName().toString().contains(namePart)) + .findFirst() + .map(Path::normalize) + .map(Path::toAbsolutePath) + .orElse(null); + } + } + + private static void unzipMatching(Path zipFile, Path outputDir, String... prefixes) throws IOException { + try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(zipFile))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.isDirectory() || !entry.getName().endsWith(".class")) { + continue; + } + if (!matchesPrefix(entry.getName(), prefixes)) { + continue; + } + Path out = outputDir.resolve(entry.getName()); + Files.createDirectories(out.getParent()); + Files.copy(zis, out, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + } + } + + private static boolean matchesPrefix(String name, String... prefixes) { + for (String prefix : prefixes) { + if (name.startsWith(prefix)) { + return true; + } + } + return false; + } +} diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptCn1CoreNativeAuditTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptCn1CoreNativeAuditTest.java new file mode 100644 index 0000000000..e59f0c42ad --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptCn1CoreNativeAuditTest.java @@ -0,0 +1,80 @@ +package com.codename1.tools.translator; + +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class JavascriptCn1CoreNativeAuditTest { + + @Test + void allCodenameOneCoreNativesAreCategorizedForJavascript() throws Exception { + Path coreJar = findDependencyJar("codenameone-core"); + assertNotNull(coreJar, "codenameone-core dependency jar should be available in target/benchmark-dependencies"); + + List uncategorized = new ArrayList(); + try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(coreJar))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.isDirectory() || !entry.getName().endsWith(".class")) { + continue; + } + if (!entry.getName().startsWith("com/codename1/")) { + continue; + } + JavascriptNativeAuditSupport.inspectClass(new NonClosingInputStream(zis), uncategorized); + } + } + + Collections.sort(uncategorized); + assertTrue(uncategorized.isEmpty(), + "Every codenameone-core native reachable by the JS VM must be categorized. Missing: " + uncategorized); + } + + private static Path findDependencyJar(String namePart) throws Exception { + Path depsDir = Paths.get("target", "benchmark-dependencies"); + if (!Files.exists(depsDir)) { + return null; + } + try (Stream paths = Files.list(depsDir)) { + return paths.filter(path -> path.getFileName().toString().contains(namePart)) + .findFirst() + .map(Path::normalize) + .map(Path::toAbsolutePath) + .orElse(null); + } + } + + private static final class NonClosingInputStream extends InputStream { + private final InputStream delegate; + + private NonClosingInputStream(InputStream delegate) { + this.delegate = delegate; + } + + @Override + public int read() throws java.io.IOException { + return delegate.read(); + } + + @Override + public int read(byte[] b, int off, int len) throws java.io.IOException { + return delegate.read(b, off, len); + } + + @Override + public void close() { + } + } +} diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptNativeAuditSupport.java b/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptNativeAuditSupport.java new file mode 100644 index 0000000000..68a4004352 --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptNativeAuditSupport.java @@ -0,0 +1,49 @@ +package com.codename1.tools.translator; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +final class JavascriptNativeAuditSupport { + + private JavascriptNativeAuditSupport() { + } + + static void inspectClass(Path classFile, final List uncategorized) { + try (InputStream input = Files.newInputStream(classFile)) { + inspectClass(input, uncategorized); + } catch (Exception ex) { + throw new RuntimeException("Failed to inspect " + classFile, ex); + } + } + + static void inspectClass(InputStream input, final List uncategorized) throws IOException { + ClassReader reader = new ClassReader(input); + reader.accept(new ClassVisitor(Opcodes.ASM9) { + private String owner; + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + owner = name; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + if ((access & Opcodes.ACC_NATIVE) != 0) { + String symbol = JavascriptNameUtil.methodIdentifier(owner, name, descriptor); + if (JavascriptNativeRegistry.categoryFor(symbol) == JavascriptNativeRegistry.NativeCategory.UNCATEGORIZED) { + uncategorized.add(symbol); + } + } + return null; + } + }, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + } +} diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptNativeAuditTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptNativeAuditTest.java new file mode 100644 index 0000000000..58dbcf4da0 --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptNativeAuditTest.java @@ -0,0 +1,38 @@ +package com.codename1.tools.translator; + +import org.junit.jupiter.params.ParameterizedTest; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class JavascriptNativeAuditTest { + + @ParameterizedTest + @org.junit.jupiter.params.provider.MethodSource("com.codename1.tools.translator.BytecodeInstructionIntegrationTest#provideCompilerConfigs") + void allCompiledJavaApiNativesAreCategorizedForJavascript(CompilerHelper.CompilerConfig config) throws Exception { + assertTrue(CompilerHelper.isJavaApiCompatible(config), + "JDK " + config.jdkVersion + " must target matching bytecode level for JavaAPI"); + + Path javaApiDir = Files.createTempDirectory("java-api-native-audit"); + CompilerHelper.compileJavaAPI(javaApiDir, config); + + final List uncategorized = new ArrayList(); + try (Stream paths = Files.walk(javaApiDir)) { + paths.filter(path -> path.toString().endsWith(".class")) + .forEach(path -> inspectClass(path, uncategorized)); + } + + Collections.sort(uncategorized); + assertTrue(uncategorized.isEmpty(), + "Every compiled JavaAPI native must be categorized for javascript backend. Missing: " + uncategorized); + } + + private static void inspectClass(Path classFile, final List uncategorized) { + JavascriptNativeAuditSupport.inspectClass(classFile, uncategorized); + } +} diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptOpcodeCoverageTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptOpcodeCoverageTest.java new file mode 100644 index 0000000000..e4c95942ac --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptOpcodeCoverageTest.java @@ -0,0 +1,379 @@ +package com.codename1.tools.translator; + +import org.junit.jupiter.api.Test; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class JavascriptOpcodeCoverageTest { + + @Test + void translatesStackAndPrimitiveCoverageFixture() throws Exception { + Parser.cleanup(); + + Path classesDir = Files.createTempDirectory("js-opcode-classes"); + writeCoverageClass(classesDir.resolve("JsOpcodeCoverage.class")); + + Path outputDir = Files.createTempDirectory("js-opcode-output"); + JavascriptTargetIntegrationTest.runJavascriptTranslator(classesDir, outputDir, "JsOpcodeCoverage"); + + Path distDir = outputDir.resolve("dist").resolve("JsOpcodeCoverage-js"); + String translatedApp = new String(Files.readAllBytes(distDir.resolve("translated_app.js")), StandardCharsets.UTF_8); + + assertTrue(translatedApp.contains("stackFamily"), "Coverage fixture should translate stack-family methods"); + assertTrue(translatedApp.contains("primitiveComparisons"), "Coverage fixture should translate primitive compare methods"); + } + + @Test + void translatesMonitorAndWaitCoverageFixture() throws Exception { + Parser.cleanup(); + + Path classesDir = Files.createTempDirectory("js-monitor-classes"); + writeMonitorCoverageClass(classesDir.resolve("JsMonitorCoverage.class")); + + Path outputDir = Files.createTempDirectory("js-monitor-output"); + JavascriptTargetIntegrationTest.runJavascriptTranslator(classesDir, outputDir, "JsMonitorCoverage"); + + Path distDir = outputDir.resolve("dist").resolve("JsMonitorCoverage-js"); + String translatedApp = new String(Files.readAllBytes(distDir.resolve("translated_app.js")), StandardCharsets.UTF_8); + String runtime = new String(Files.readAllBytes(distDir.resolve("parparvm_runtime.js")), StandardCharsets.UTF_8); + + assertTrue(translatedApp.contains("monitorBlock"), "Coverage fixture should translate monitorenter/monitorexit methods"); + assertTrue(translatedApp.contains("waitAndNotify"), "Coverage fixture should translate wait/notify methods"); + assertTrue(translatedApp.contains("sleepOnce"), "Coverage fixture should translate sleep methods"); + assertTrue(runtime.contains("waitOn(thread, obj, timeout)"), "Runtime should expose cooperative wait support"); + assertTrue(runtime.contains("cn1_java_lang_Object_wait_long_int") || runtime.contains("cn1_java_lang_Object_wait___long_int"), + "Runtime should expose wait() native support"); + assertTrue(runtime.contains("cn1_java_lang_Object_notifyAll") || runtime.contains("cn1_java_lang_Object_notifyAll__"), + "Runtime should expose notifyAll() native support"); + assertTrue(runtime.contains("cn1_java_lang_Thread_sleep_long") || runtime.contains("cn1_java_lang_Thread_sleep___long"), + "Runtime should expose sleep() native support"); + } + + @Test + void translatesObjectTypeAndDispatchCoverageFixture() throws Exception { + Parser.cleanup(); + + Path classesDir = Files.createTempDirectory("js-type-classes"); + writeInterfaceClass(classesDir.resolve("JsTypeIface.class")); + writeBaseClass(classesDir.resolve("JsTypeBase.class")); + writeImplClass(classesDir.resolve("JsTypeImpl.class")); + writeTypeCoverageClass(classesDir.resolve("JsTypeCoverage.class")); + + Path outputDir = Files.createTempDirectory("js-type-output"); + JavascriptTargetIntegrationTest.runJavascriptTranslator(classesDir, outputDir, "JsTypeCoverage"); + + Path distDir = outputDir.resolve("dist").resolve("JsTypeCoverage-js"); + String translatedApp = new String(Files.readAllBytes(distDir.resolve("translated_app.js")), StandardCharsets.UTF_8); + String runtime = new String(Files.readAllBytes(distDir.resolve("parparvm_runtime.js")), StandardCharsets.UTF_8); + + assertTrue(translatedApp.contains("castsAndTypes"), "Coverage fixture should translate CHECKCAST/INSTANCEOF methods"); + assertTrue(translatedApp.contains("dispatch"), "Coverage fixture should translate virtual/interface dispatch methods"); + assertTrue(translatedApp.contains("jvm.getClassObject(\"JsTypeImpl\")"), "Coverage fixture should translate class literals"); + assertTrue(translatedApp.contains("assignableTo: {") + && translatedApp.contains("\"JsTypeImpl\": true") + && translatedApp.contains("\"JsTypeBase\": true") + && translatedApp.contains("\"JsTypeIface\": true"), + "Class metadata should include static assignability information"); + assertTrue(translatedApp.contains("jvm.classes[__target.__class] && jvm.classes[__target.__class].methods") + && translatedApp.contains("jvm.classes[__target.__class].methods["), + "Virtual/interface dispatch should use an exact-class method-table fast path"); + assertTrue(translatedApp.contains("jvm.resolveVirtual(__target.__class"), "Dispatch should retain inheritance/interface fallback"); + assertTrue(runtime.contains("resolveVirtual(className, methodId)"), "Runtime should resolve virtual methods by class name"); + assertTrue(runtime.contains("obj.__classDef.assignableTo[className]"), "Runtime instanceof should use emitted class assignability tables"); + assertTrue(runtime.contains("arrayAssignableTo(componentClass, dimensions)") && runtime.contains("isPrimitiveComponent(componentClass)"), + "Runtime should keep array assignability limited to CN1-relevant cases"); + } + + private static void writeCoverageClass(Path target) throws Exception { + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, "JsOpcodeCoverage", null, "java/lang/Object", null); + + MethodVisitor init = cw.visitMethod(Opcodes.ACC_PUBLIC, "", "()V", null, null); + init.visitCode(); + init.visitVarInsn(Opcodes.ALOAD, 0); + init.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V", false); + init.visitInsn(Opcodes.RETURN); + init.visitMaxs(0, 0); + init.visitEnd(); + + MethodVisitor main = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); + main.visitCode(); + main.visitMethodInsn(Opcodes.INVOKESTATIC, "JsOpcodeCoverage", "stackFamily", "()V", false); + main.visitMethodInsn(Opcodes.INVOKESTATIC, "JsOpcodeCoverage", "primitiveComparisons", "()V", false); + main.visitInsn(Opcodes.RETURN); + main.visitMaxs(0, 0); + main.visitEnd(); + + MethodVisitor stack = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "stackFamily", "()V", null, null); + stack.visitCode(); + stack.visitInsn(Opcodes.ICONST_1); + stack.visitInsn(Opcodes.ICONST_2); + stack.visitInsn(Opcodes.DUP_X1); + stack.visitInsn(Opcodes.POP); + stack.visitInsn(Opcodes.POP2); + + stack.visitInsn(Opcodes.ICONST_1); + stack.visitInsn(Opcodes.ICONST_2); + stack.visitInsn(Opcodes.ICONST_3); + stack.visitInsn(Opcodes.DUP_X2); + stack.visitInsn(Opcodes.POP); + stack.visitInsn(Opcodes.POP2); + stack.visitInsn(Opcodes.POP); + + stack.visitInsn(Opcodes.ICONST_1); + stack.visitInsn(Opcodes.ICONST_2); + stack.visitInsn(Opcodes.DUP2); + stack.visitInsn(Opcodes.POP2); + stack.visitInsn(Opcodes.POP2); + + stack.visitInsn(Opcodes.ICONST_1); + stack.visitInsn(Opcodes.ICONST_2); + stack.visitInsn(Opcodes.ICONST_3); + stack.visitInsn(Opcodes.DUP2_X1); + stack.visitInsn(Opcodes.POP); + stack.visitInsn(Opcodes.POP2); + stack.visitInsn(Opcodes.POP2); + + stack.visitInsn(Opcodes.ICONST_1); + stack.visitInsn(Opcodes.ICONST_2); + stack.visitInsn(Opcodes.ICONST_3); + stack.visitInsn(Opcodes.ICONST_4); + stack.visitInsn(Opcodes.DUP2_X2); + stack.visitInsn(Opcodes.POP2); + stack.visitInsn(Opcodes.POP2); + stack.visitInsn(Opcodes.POP2); + + stack.visitInsn(Opcodes.ICONST_1); + stack.visitInsn(Opcodes.ICONST_2); + stack.visitInsn(Opcodes.SWAP); + stack.visitInsn(Opcodes.POP2); + stack.visitInsn(Opcodes.RETURN); + stack.visitMaxs(0, 0); + stack.visitEnd(); + + MethodVisitor primitive = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "primitiveComparisons", "()V", null, null); + primitive.visitCode(); + primitive.visitInsn(Opcodes.FCONST_0); + primitive.visitInsn(Opcodes.FCONST_1); + primitive.visitInsn(Opcodes.FCMPL); + primitive.visitInsn(Opcodes.POP); + primitive.visitInsn(Opcodes.FCONST_2); + primitive.visitInsn(Opcodes.FCONST_1); + primitive.visitInsn(Opcodes.FCMPG); + primitive.visitInsn(Opcodes.POP); + primitive.visitInsn(Opcodes.DCONST_0); + primitive.visitInsn(Opcodes.DCONST_1); + primitive.visitInsn(Opcodes.DCMPL); + primitive.visitInsn(Opcodes.POP); + primitive.visitInsn(Opcodes.DCONST_1); + primitive.visitInsn(Opcodes.DCONST_0); + primitive.visitInsn(Opcodes.DCMPG); + primitive.visitInsn(Opcodes.POP); + primitive.visitInsn(Opcodes.LCONST_0); + primitive.visitInsn(Opcodes.LCONST_1); + primitive.visitInsn(Opcodes.LCMP); + primitive.visitInsn(Opcodes.POP); + primitive.visitInsn(Opcodes.RETURN); + primitive.visitMaxs(0, 0); + primitive.visitEnd(); + + cw.visitEnd(); + Files.write(target, cw.toByteArray()); + } + + private static void writeMonitorCoverageClass(Path target) throws Exception { + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, "JsMonitorCoverage", null, "java/lang/Object", null); + + MethodVisitor init = cw.visitMethod(Opcodes.ACC_PUBLIC, "", "()V", null, null); + init.visitCode(); + init.visitVarInsn(Opcodes.ALOAD, 0); + init.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V", false); + init.visitInsn(Opcodes.RETURN); + init.visitMaxs(0, 0); + init.visitEnd(); + + MethodVisitor main = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, new String[]{"java/lang/Exception"}); + main.visitCode(); + main.visitTypeInsn(Opcodes.NEW, "java/lang/Object"); + main.visitInsn(Opcodes.DUP); + main.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V", false); + main.visitVarInsn(Opcodes.ASTORE, 1); + main.visitVarInsn(Opcodes.ALOAD, 1); + main.visitMethodInsn(Opcodes.INVOKESTATIC, "JsMonitorCoverage", "monitorBlock", "(Ljava/lang/Object;)V", false); + main.visitVarInsn(Opcodes.ALOAD, 1); + main.visitMethodInsn(Opcodes.INVOKESTATIC, "JsMonitorCoverage", "waitAndNotify", "(Ljava/lang/Object;)V", false); + main.visitMethodInsn(Opcodes.INVOKESTATIC, "JsMonitorCoverage", "sleepOnce", "()V", false); + main.visitInsn(Opcodes.RETURN); + main.visitMaxs(0, 0); + main.visitEnd(); + + MethodVisitor monitor = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "monitorBlock", "(Ljava/lang/Object;)V", null, null); + monitor.visitCode(); + monitor.visitVarInsn(Opcodes.ALOAD, 0); + monitor.visitInsn(Opcodes.MONITORENTER); + monitor.visitVarInsn(Opcodes.ALOAD, 0); + monitor.visitInsn(Opcodes.MONITOREXIT); + monitor.visitInsn(Opcodes.RETURN); + monitor.visitMaxs(0, 0); + monitor.visitEnd(); + + MethodVisitor waitNotify = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "waitAndNotify", "(Ljava/lang/Object;)V", null, new String[]{"java/lang/InterruptedException"}); + waitNotify.visitCode(); + waitNotify.visitVarInsn(Opcodes.ALOAD, 0); + waitNotify.visitInsn(Opcodes.MONITORENTER); + waitNotify.visitVarInsn(Opcodes.ALOAD, 0); + waitNotify.visitInsn(Opcodes.LCONST_0); + waitNotify.visitInsn(Opcodes.ICONST_0); + waitNotify.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "wait", "(JI)V", false); + waitNotify.visitVarInsn(Opcodes.ALOAD, 0); + waitNotify.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "notifyAll", "()V", false); + waitNotify.visitVarInsn(Opcodes.ALOAD, 0); + waitNotify.visitInsn(Opcodes.MONITOREXIT); + waitNotify.visitInsn(Opcodes.RETURN); + waitNotify.visitMaxs(0, 0); + waitNotify.visitEnd(); + + MethodVisitor sleep = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "sleepOnce", "()V", null, new String[]{"java/lang/InterruptedException"}); + sleep.visitCode(); + sleep.visitInsn(Opcodes.LCONST_1); + sleep.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Thread", "sleep", "(J)V", false); + sleep.visitInsn(Opcodes.RETURN); + sleep.visitMaxs(0, 0); + sleep.visitEnd(); + + cw.visitEnd(); + Files.write(target, cw.toByteArray()); + } + + private static void writeInterfaceClass(Path target) throws Exception { + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT | Opcodes.ACC_INTERFACE, + "JsTypeIface", null, "java/lang/Object", null); + + MethodVisitor call = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT, "call", "()I", null, null); + call.visitEnd(); + + cw.visitEnd(); + Files.write(target, cw.toByteArray()); + } + + private static void writeBaseClass(Path target) throws Exception { + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, "JsTypeBase", null, "java/lang/Object", null); + + MethodVisitor init = cw.visitMethod(Opcodes.ACC_PUBLIC, "", "()V", null, null); + init.visitCode(); + init.visitVarInsn(Opcodes.ALOAD, 0); + init.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V", false); + init.visitInsn(Opcodes.RETURN); + init.visitMaxs(0, 0); + init.visitEnd(); + + MethodVisitor value = cw.visitMethod(Opcodes.ACC_PUBLIC, "value", "()I", null, null); + value.visitCode(); + value.visitIntInsn(Opcodes.BIPUSH, 7); + value.visitInsn(Opcodes.IRETURN); + value.visitMaxs(0, 0); + value.visitEnd(); + + cw.visitEnd(); + Files.write(target, cw.toByteArray()); + } + + private static void writeImplClass(Path target) throws Exception { + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, "JsTypeImpl", null, "JsTypeBase", new String[]{"JsTypeIface"}); + + MethodVisitor init = cw.visitMethod(Opcodes.ACC_PUBLIC, "", "()V", null, null); + init.visitCode(); + init.visitVarInsn(Opcodes.ALOAD, 0); + init.visitMethodInsn(Opcodes.INVOKESPECIAL, "JsTypeBase", "", "()V", false); + init.visitInsn(Opcodes.RETURN); + init.visitMaxs(0, 0); + init.visitEnd(); + + MethodVisitor value = cw.visitMethod(Opcodes.ACC_PUBLIC, "value", "()I", null, null); + value.visitCode(); + value.visitIntInsn(Opcodes.BIPUSH, 11); + value.visitInsn(Opcodes.IRETURN); + value.visitMaxs(0, 0); + value.visitEnd(); + + MethodVisitor call = cw.visitMethod(Opcodes.ACC_PUBLIC, "call", "()I", null, null); + call.visitCode(); + call.visitIntInsn(Opcodes.BIPUSH, 13); + call.visitInsn(Opcodes.IRETURN); + call.visitMaxs(0, 0); + call.visitEnd(); + + cw.visitEnd(); + Files.write(target, cw.toByteArray()); + } + + private static void writeTypeCoverageClass(Path target) throws Exception { + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, "JsTypeCoverage", null, "java/lang/Object", null); + + MethodVisitor init = cw.visitMethod(Opcodes.ACC_PUBLIC, "", "()V", null, null); + init.visitCode(); + init.visitVarInsn(Opcodes.ALOAD, 0); + init.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V", false); + init.visitInsn(Opcodes.RETURN); + init.visitMaxs(0, 0); + init.visitEnd(); + + MethodVisitor main = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); + main.visitCode(); + main.visitTypeInsn(Opcodes.NEW, "JsTypeImpl"); + main.visitInsn(Opcodes.DUP); + main.visitMethodInsn(Opcodes.INVOKESPECIAL, "JsTypeImpl", "", "()V", false); + main.visitVarInsn(Opcodes.ASTORE, 1); + main.visitVarInsn(Opcodes.ALOAD, 1); + main.visitMethodInsn(Opcodes.INVOKESTATIC, "JsTypeCoverage", "castsAndTypes", "(Ljava/lang/Object;)V", false); + main.visitMethodInsn(Opcodes.INVOKESTATIC, "JsTypeCoverage", "dispatch", "()V", false); + main.visitInsn(Opcodes.RETURN); + main.visitMaxs(0, 0); + main.visitEnd(); + + MethodVisitor casts = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "castsAndTypes", "(Ljava/lang/Object;)V", null, null); + casts.visitCode(); + casts.visitVarInsn(Opcodes.ALOAD, 0); + casts.visitTypeInsn(Opcodes.CHECKCAST, "JsTypeImpl"); + casts.visitInsn(Opcodes.POP); + casts.visitVarInsn(Opcodes.ALOAD, 0); + casts.visitTypeInsn(Opcodes.INSTANCEOF, "JsTypeImpl"); + casts.visitInsn(Opcodes.POP); + casts.visitLdcInsn(org.objectweb.asm.Type.getObjectType("JsTypeImpl")); + casts.visitInsn(Opcodes.POP); + casts.visitInsn(Opcodes.RETURN); + casts.visitMaxs(0, 0); + casts.visitEnd(); + + MethodVisitor dispatch = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "dispatch", "()V", null, null); + dispatch.visitCode(); + dispatch.visitTypeInsn(Opcodes.NEW, "JsTypeImpl"); + dispatch.visitInsn(Opcodes.DUP); + dispatch.visitMethodInsn(Opcodes.INVOKESPECIAL, "JsTypeImpl", "", "()V", false); + dispatch.visitVarInsn(Opcodes.ASTORE, 0); + dispatch.visitVarInsn(Opcodes.ALOAD, 0); + dispatch.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "JsTypeBase", "value", "()I", false); + dispatch.visitVarInsn(Opcodes.ALOAD, 0); + dispatch.visitMethodInsn(Opcodes.INVOKEINTERFACE, "JsTypeIface", "call", "()I", true); + dispatch.visitInsn(Opcodes.IADD); + dispatch.visitInsn(Opcodes.POP); + dispatch.visitInsn(Opcodes.RETURN); + dispatch.visitMaxs(0, 0); + dispatch.visitEnd(); + + cw.visitEnd(); + Files.write(target, cw.toByteArray()); + } +} diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptRuntimeSemanticsTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptRuntimeSemanticsTest.java new file mode 100644 index 0000000000..a2f2d296f6 --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptRuntimeSemanticsTest.java @@ -0,0 +1,877 @@ +package com.codename1.tools.translator; + +import org.junit.jupiter.params.ParameterizedTest; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class JavascriptRuntimeSemanticsTest { + + @ParameterizedTest + @org.junit.jupiter.params.provider.MethodSource("com.codename1.tools.translator.BytecodeInstructionIntegrationTest#provideCompilerConfigs") + void executesArrayCovarianceInWorkerRuntime(CompilerHelper.CompilerConfig config) throws Exception { + WorkerRunResult result = translateAndRunFixture(config, "JsArrayCovarianceApp.java", "JsArrayCovarianceApp"); + + assertEquals(511, result.result, "Translated runtime should preserve CN1-relevant array covariance semantics"); + assertTrue(result.errorMessage == null || result.errorMessage.isEmpty(), "Worker should not emit an error message"); + } + + @ParameterizedTest + @org.junit.jupiter.params.provider.MethodSource("com.codename1.tools.translator.BytecodeInstructionIntegrationTest#provideCompilerConfigs") + void executesLocaleTimeZoneAndDateFormatInWorkerRuntime(CompilerHelper.CompilerConfig config) throws Exception { + WorkerRunResult result = translateAndRunFixture(config, "JsLocaleTimeZoneApp.java", "JsLocaleTimeZoneApp"); + + assertEquals(511, result.result, "Translated runtime should preserve browser-safe Locale/TimeZone/DateFormat semantics"); + assertTrue(result.errorMessage == null || result.errorMessage.isEmpty(), "Worker should not emit an error message"); + } + + @ParameterizedTest + @org.junit.jupiter.params.provider.MethodSource("com.codename1.tools.translator.BytecodeInstructionIntegrationTest#provideCompilerConfigs") + void executesThreadWaitSleepJoinAndInterruptInWorkerRuntime(CompilerHelper.CompilerConfig config) throws Exception { + WorkerRunResult result = translateAndRunFixture(config, "JsThreadSemanticsApp.java", "JsThreadSemanticsApp"); + + assertEquals(32717, result.result, "Translated runtime should preserve CN1-relevant thread semantics"); + assertTrue(result.errorMessage == null || result.errorMessage.isEmpty(), "Worker should not emit an error message"); + } + + @ParameterizedTest + @org.junit.jupiter.params.provider.MethodSource("com.codename1.tools.translator.BytecodeInstructionIntegrationTest#provideCompilerConfigs") + void executesBroaderJavaApiCoverageInWorkerRuntime(CompilerHelper.CompilerConfig config) throws Exception { + WorkerRunResult result = translateAndRunFixture(config, "JsJavaApiCoverageApp.java", "JsJavaApiCoverageApp"); + + assertEquals(255, result.result, "Translated runtime should execute the broader JavaAPI coverage fixture"); + assertTrue(result.errorMessage == null || result.errorMessage.isEmpty(), "Worker should not emit an error message"); + } + + @ParameterizedTest + @org.junit.jupiter.params.provider.MethodSource("com.codename1.tools.translator.BytecodeInstructionIntegrationTest#provideCompilerConfigs") + void executesHostCallbacksThroughWorkerProtocol(CompilerHelper.CompilerConfig config) throws Exception { + Parser.cleanup(); + + Path sourceDir = Files.createTempDirectory("js-host-src"); + Path classesDir = Files.createTempDirectory("js-host-classes"); + Path javaApiDir = Files.createTempDirectory("js-host-javaapi"); + + Path vmHostDir = sourceDir.resolve("com").resolve("codename1").resolve("impl").resolve("platform").resolve("js"); + Files.createDirectories(vmHostDir); + Files.write(vmHostDir.resolve("VMHost.java"), + JavascriptTargetIntegrationTest.loadFixture("com/codename1/impl/platform/js/VMHost.java").getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("JsHostCallbackApp.java"), + JavascriptTargetIntegrationTest.loadFixture("JsHostCallbackApp.java").getBytes(StandardCharsets.UTF_8)); + + JavascriptTargetIntegrationTest.compileAgainstJavaApi(config, sourceDir, classesDir, javaApiDir); + + Path outputDir = Files.createTempDirectory("js-host-output"); + JavascriptTargetIntegrationTest.runJavascriptTranslator(classesDir, outputDir, "JsHostCallbackApp"); + + Path distDir = outputDir.resolve("dist").resolve("JsHostCallbackApp-js"); + WorkerRunResult result = runGeneratedWorkerBundleWithHostCallbacks(distDir); + + assertEquals("result", result.type, "Generated worker bundle should complete through the host callback protocol"); + assertEquals(42, result.result, "Host callback should round-trip data back into the translated VM"); + assertTrue(result.errorMessage == null || result.errorMessage.isEmpty(), "Generated worker bundle should not emit an error message"); + } + + @ParameterizedTest + @org.junit.jupiter.params.provider.MethodSource("com.codename1.tools.translator.BytecodeInstructionIntegrationTest#provideCompilerConfigs") + void injectsEventsAndIgnoresUnknownMessagesThroughWorkerProtocol(CompilerHelper.CompilerConfig config) throws Exception { + Parser.cleanup(); + + Path sourceDir = Files.createTempDirectory("js-host-event-src"); + Path classesDir = Files.createTempDirectory("js-host-event-classes"); + Path javaApiDir = Files.createTempDirectory("js-host-event-javaapi"); + + Path vmHostDir = sourceDir.resolve("com").resolve("codename1").resolve("impl").resolve("platform").resolve("js"); + Files.createDirectories(vmHostDir); + Files.write(vmHostDir.resolve("VMHost.java"), + JavascriptTargetIntegrationTest.loadFixture("com/codename1/impl/platform/js/VMHost.java").getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("JsHostEventApp.java"), + JavascriptTargetIntegrationTest.loadFixture("JsHostEventApp.java").getBytes(StandardCharsets.UTF_8)); + + JavascriptTargetIntegrationTest.compileAgainstJavaApi(config, sourceDir, classesDir, javaApiDir); + + Path outputDir = Files.createTempDirectory("js-host-event-output"); + JavascriptTargetIntegrationTest.runJavascriptTranslator(classesDir, outputDir, "JsHostEventApp"); + + Path distDir = outputDir.resolve("dist").resolve("JsHostEventApp-js"); + WorkerRunResult result = runGeneratedWorkerBundleWithEventInjection(distDir); + + assertEquals("result", result.type, "Generated worker bundle should complete after host event injection"); + assertEquals(4142, result.result, "Worker should preserve host-injected event data and ignore unknown protocol messages"); + assertTrue(result.errorMessage == null || result.errorMessage.isEmpty(), "Generated worker bundle should not emit an error message"); + } + + @ParameterizedTest + @org.junit.jupiter.params.provider.MethodSource("com.codename1.tools.translator.BytecodeInstructionIntegrationTest#provideCompilerConfigs") + void preservesHostEventQueueOrderingThroughWorkerProtocol(CompilerHelper.CompilerConfig config) throws Exception { + Parser.cleanup(); + + Path sourceDir = Files.createTempDirectory("js-host-event-queue-src"); + Path classesDir = Files.createTempDirectory("js-host-event-queue-classes"); + Path javaApiDir = Files.createTempDirectory("js-host-event-queue-javaapi"); + + Path vmHostDir = sourceDir.resolve("com").resolve("codename1").resolve("impl").resolve("platform").resolve("js"); + Files.createDirectories(vmHostDir); + Files.write(vmHostDir.resolve("VMHost.java"), + JavascriptTargetIntegrationTest.loadFixture("com/codename1/impl/platform/js/VMHost.java").getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("JsHostEventQueueApp.java"), + JavascriptTargetIntegrationTest.loadFixture("JsHostEventQueueApp.java").getBytes(StandardCharsets.UTF_8)); + + JavascriptTargetIntegrationTest.compileAgainstJavaApi(config, sourceDir, classesDir, javaApiDir); + + Path outputDir = Files.createTempDirectory("js-host-event-queue-output"); + JavascriptTargetIntegrationTest.runJavascriptTranslator(classesDir, outputDir, "JsHostEventQueueApp"); + + Path distDir = outputDir.resolve("dist").resolve("JsHostEventQueueApp-js"); + WorkerRunResult result = runGeneratedWorkerBundleWithQueuedEvents(distDir); + + assertEquals("result", result.type, "Generated worker bundle should complete after queued host events"); + assertEquals(4120, result.result, "Worker should preserve FIFO event ordering and return -1 when the queue is empty"); + assertTrue(result.errorMessage == null || result.errorMessage.isEmpty(), "Generated worker bundle should not emit an error message"); + } + + @ParameterizedTest + @org.junit.jupiter.params.provider.MethodSource("com.codename1.tools.translator.BytecodeInstructionIntegrationTest#provideCompilerConfigs") + void propagatesHostCallbackErrorsDeterministicallyThroughWorkerProtocol(CompilerHelper.CompilerConfig config) throws Exception { + Parser.cleanup(); + + Path sourceDir = Files.createTempDirectory("js-host-error-src"); + Path classesDir = Files.createTempDirectory("js-host-error-classes"); + Path javaApiDir = Files.createTempDirectory("js-host-error-javaapi"); + + Path vmHostDir = sourceDir.resolve("com").resolve("codename1").resolve("impl").resolve("platform").resolve("js"); + Files.createDirectories(vmHostDir); + Files.write(vmHostDir.resolve("VMHost.java"), + JavascriptTargetIntegrationTest.loadFixture("com/codename1/impl/platform/js/VMHost.java").getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("JsHostCallbackErrorApp.java"), + JavascriptTargetIntegrationTest.loadFixture("JsHostCallbackErrorApp.java").getBytes(StandardCharsets.UTF_8)); + + JavascriptTargetIntegrationTest.compileAgainstJavaApi(config, sourceDir, classesDir, javaApiDir); + + Path outputDir = Files.createTempDirectory("js-host-error-output"); + JavascriptTargetIntegrationTest.runJavascriptTranslator(classesDir, outputDir, "JsHostCallbackErrorApp"); + + Path distDir = outputDir.resolve("dist").resolve("JsHostCallbackErrorApp-js"); + WorkerRunResult result = runGeneratedWorkerBundleWithHostCallbackError(distDir); + + assertEquals("error", result.type, "Generated worker bundle should surface host callback failures as worker errors"); + assertTrue(result.errorMessage != null && result.errorMessage.contains("Injected host failure 7"), + "Worker should expose a deterministic host callback error message"); + } + + @ParameterizedTest + @org.junit.jupiter.params.provider.MethodSource("com.codename1.tools.translator.BytecodeInstructionIntegrationTest#provideCompilerConfigs") + void executesGeneratedWorkerProtocolEndToEnd(CompilerHelper.CompilerConfig config) throws Exception { + Parser.cleanup(); + + Path sourceDir = Files.createTempDirectory("js-worker-src"); + Path classesDir = Files.createTempDirectory("js-worker-classes"); + Path javaApiDir = Files.createTempDirectory("js-worker-javaapi"); + + Files.write(sourceDir.resolve("JsWorkerProtocolApp.java"), + JavascriptTargetIntegrationTest.loadFixture("JsWorkerProtocolApp.java").getBytes(StandardCharsets.UTF_8)); + + JavascriptTargetIntegrationTest.compileAgainstJavaApi(config, sourceDir, classesDir, javaApiDir); + + Path outputDir = Files.createTempDirectory("js-worker-output"); + JavascriptTargetIntegrationTest.runJavascriptTranslator(classesDir, outputDir, "JsWorkerProtocolApp"); + + Path distDir = outputDir.resolve("dist").resolve("JsWorkerProtocolApp-js"); + WorkerRunResult result = runGeneratedWorkerBundle(distDir); + + assertEquals("result", result.type, "Generated worker bundle should report completion through the worker protocol"); + assertEquals(321, result.result, "Generated worker bundle should execute start/result flow end-to-end"); + assertTrue(result.errorMessage == null || result.errorMessage.isEmpty(), "Generated worker bundle should not emit an error message"); + } + + @ParameterizedTest + @org.junit.jupiter.params.provider.MethodSource("com.codename1.tools.translator.BytecodeInstructionIntegrationTest#provideCompilerConfigs") + void exposesStableVmProtocolHandshakeBeforeWorkerStart(CompilerHelper.CompilerConfig config) throws Exception { + Parser.cleanup(); + + Path sourceDir = Files.createTempDirectory("js-worker-protocol-src"); + Path classesDir = Files.createTempDirectory("js-worker-protocol-classes"); + Path javaApiDir = Files.createTempDirectory("js-worker-protocol-javaapi"); + + Files.write(sourceDir.resolve("JsWorkerProtocolApp.java"), + JavascriptTargetIntegrationTest.loadFixture("JsWorkerProtocolApp.java").getBytes(StandardCharsets.UTF_8)); + + JavascriptTargetIntegrationTest.compileAgainstJavaApi(config, sourceDir, classesDir, javaApiDir); + + Path outputDir = Files.createTempDirectory("js-worker-protocol-output"); + JavascriptTargetIntegrationTest.runJavascriptTranslator(classesDir, outputDir, "JsWorkerProtocolApp"); + + Path distDir = outputDir.resolve("dist").resolve("JsWorkerProtocolApp-js"); + WorkerRunResult result = runGeneratedWorkerBundleWithProtocolHandshake(distDir); + + assertEquals("protocol-check", result.type, "Generated worker bundle should expose the VM protocol before start"); + assertEquals(1, result.protocolVersion, "VM protocol version should be stable and explicit"); + assertEquals("start", result.protocolStartType, "VM protocol should document the start message"); + assertEquals("host-callback", result.protocolHostCallbackType, "VM protocol should document host callback delivery"); + assertEquals("timer-wake", result.protocolTimerWakeType, "VM protocol should document timer wake delivery"); + assertEquals(321, result.result, "Worker should still execute normally after protocol handshake"); + assertTrue(result.errorMessage == null || result.errorMessage.isEmpty(), "Worker should not emit an error message during protocol handshake"); + } + + private static WorkerRunResult translateAndRunFixture(CompilerHelper.CompilerConfig config, String fixtureName, String appName) throws Exception { + Parser.cleanup(); + + Path sourceDir = Files.createTempDirectory("js-runtime-src"); + Path classesDir = Files.createTempDirectory("js-runtime-classes"); + Path javaApiDir = Files.createTempDirectory("js-runtime-javaapi"); + + Files.write(sourceDir.resolve(appName + ".java"), + JavascriptTargetIntegrationTest.loadFixture(fixtureName).getBytes(StandardCharsets.UTF_8)); + + JavascriptTargetIntegrationTest.compileAgainstJavaApi(config, sourceDir, classesDir, javaApiDir); + + Path outputDir = Files.createTempDirectory("js-runtime-output"); + JavascriptTargetIntegrationTest.runJavascriptTranslator(classesDir, outputDir, appName); + + Path distDir = outputDir.resolve("dist").resolve(appName + "-js"); + return runWorkerBundle(distDir, appName); + } + + private static WorkerRunResult runWorkerBundle(Path distDir, String appName) throws Exception { + Path harness = Files.createTempFile("js-worker-runtime", ".js"); + Files.write(harness, workerHarnessSource(distDir, appName).getBytes(StandardCharsets.UTF_8)); + Process process = new ProcessBuilder("node", harness.toString()).start(); + String output = readAll(process.getInputStream()); + String errors = readAll(process.getErrorStream()); + int rc = process.waitFor(); + assertEquals(0, rc, "Node worker harness should exit cleanly. stderr: " + errors); + WorkerRunResult out = new WorkerRunResult(); + out.rawMessage = output.trim(); + out.type = extractJsonString(output, "type"); + String result = extractJsonNumber(output, "result"); + out.result = result == null ? Integer.MIN_VALUE : Integer.parseInt(result); + out.errorMessage = extractJsonString(output, "message"); + return out; + } + + private static WorkerRunResult runGeneratedWorkerBundle(Path distDir) throws Exception { + Path harness = Files.createTempFile("js-worker-protocol", ".js"); + Files.write(harness, generatedWorkerHarnessSource(distDir).getBytes(StandardCharsets.UTF_8)); + Process process = new ProcessBuilder("node", harness.toString()).start(); + String output = readAll(process.getInputStream()); + String errors = readAll(process.getErrorStream()); + int rc = process.waitFor(); + assertEquals(0, rc, "Node worker-thread harness should exit cleanly. stderr: " + errors); + WorkerRunResult out = new WorkerRunResult(); + out.rawMessage = output.trim(); + out.type = extractJsonString(output, "type"); + String result = extractJsonNumber(output, "result"); + out.result = result == null ? Integer.MIN_VALUE : Integer.parseInt(result); + out.errorMessage = extractJsonString(output, "message"); + return out; + } + + private static WorkerRunResult runGeneratedWorkerBundleWithHostCallbacks(Path distDir) throws Exception { + Path harness = Files.createTempFile("js-worker-host-protocol", ".js"); + Files.write(harness, generatedWorkerHarnessSourceWithHostCallbacks(distDir).getBytes(StandardCharsets.UTF_8)); + Process process = new ProcessBuilder("node", harness.toString()).start(); + String output = readAll(process.getInputStream()); + String errors = readAll(process.getErrorStream()); + int rc = process.waitFor(); + assertEquals(0, rc, "Node worker-thread host harness should exit cleanly. stderr: " + errors); + WorkerRunResult out = new WorkerRunResult(); + out.rawMessage = output.trim(); + out.type = extractJsonString(output, "type"); + String result = extractJsonNumber(output, "result"); + out.result = result == null ? Integer.MIN_VALUE : Integer.parseInt(result); + out.errorMessage = extractJsonString(output, "message"); + return out; + } + + private static WorkerRunResult runGeneratedWorkerBundleWithProtocolHandshake(Path distDir) throws Exception { + Path harness = Files.createTempFile("js-worker-protocol-handshake", ".js"); + Files.write(harness, generatedWorkerHarnessSourceWithProtocolHandshake(distDir).getBytes(StandardCharsets.UTF_8)); + Process process = new ProcessBuilder("node", harness.toString()).start(); + String output = readAll(process.getInputStream()); + String errors = readAll(process.getErrorStream()); + int rc = process.waitFor(); + assertEquals(0, rc, "Node worker-thread protocol handshake harness should exit cleanly. stderr: " + errors); + WorkerRunResult out = new WorkerRunResult(); + out.rawMessage = output.trim(); + out.type = extractJsonString(output, "type"); + String result = extractJsonNumber(output, "result"); + out.result = result == null ? Integer.MIN_VALUE : Integer.parseInt(result); + String version = extractJsonNumber(output, "version"); + out.protocolVersion = version == null ? Integer.MIN_VALUE : Integer.parseInt(version); + out.protocolStartType = extractJsonString(output, "startType"); + out.protocolHostCallbackType = extractJsonString(output, "hostCallbackType"); + out.protocolTimerWakeType = extractJsonString(output, "timerWakeType"); + out.errorMessage = extractJsonString(output, "message"); + return out; + } + + private static WorkerRunResult runGeneratedWorkerBundleWithEventInjection(Path distDir) throws Exception { + Path harness = Files.createTempFile("js-worker-event-protocol", ".js"); + Files.write(harness, generatedWorkerHarnessSourceWithEventInjection(distDir).getBytes(StandardCharsets.UTF_8)); + Process process = new ProcessBuilder("node", harness.toString()).start(); + String output = readAll(process.getInputStream()); + String errors = readAll(process.getErrorStream()); + int rc = process.waitFor(); + assertEquals(0, rc, "Node worker-thread event harness should exit cleanly. stderr: " + errors); + WorkerRunResult out = new WorkerRunResult(); + out.rawMessage = output.trim(); + out.type = extractJsonString(output, "type"); + String result = extractJsonNumber(output, "result"); + out.result = result == null ? Integer.MIN_VALUE : Integer.parseInt(result); + out.errorMessage = extractJsonString(output, "message"); + return out; + } + + private static WorkerRunResult runGeneratedWorkerBundleWithQueuedEvents(Path distDir) throws Exception { + Path harness = Files.createTempFile("js-worker-queued-events", ".js"); + Files.write(harness, generatedWorkerHarnessSourceWithQueuedEvents(distDir).getBytes(StandardCharsets.UTF_8)); + Process process = new ProcessBuilder("node", harness.toString()).start(); + String output = readAll(process.getInputStream()); + String errors = readAll(process.getErrorStream()); + int rc = process.waitFor(); + assertEquals(0, rc, "Node worker-thread queued-event harness should exit cleanly. stderr: " + errors); + WorkerRunResult out = new WorkerRunResult(); + out.rawMessage = output.trim(); + out.type = extractJsonString(output, "type"); + String result = extractJsonNumber(output, "result"); + out.result = result == null ? Integer.MIN_VALUE : Integer.parseInt(result); + out.errorMessage = extractJsonString(output, "message"); + return out; + } + + private static WorkerRunResult runGeneratedWorkerBundleWithHostCallbackError(Path distDir) throws Exception { + Path harness = Files.createTempFile("js-worker-host-error", ".js"); + Files.write(harness, generatedWorkerHarnessSourceWithHostCallbackError(distDir).getBytes(StandardCharsets.UTF_8)); + Process process = new ProcessBuilder("node", harness.toString()).start(); + String output = readAll(process.getInputStream()); + String errors = readAll(process.getErrorStream()); + int rc = process.waitFor(); + assertEquals(0, rc, "Node worker-thread host-error harness should exit cleanly. stderr: " + errors); + WorkerRunResult out = new WorkerRunResult(); + out.rawMessage = output.trim(); + out.type = extractJsonString(output, "type"); + String result = extractJsonNumber(output, "result"); + out.result = result == null ? Integer.MIN_VALUE : Integer.parseInt(result); + out.errorMessage = extractJsonString(output, "message"); + return out; + } + + private static String readAll(InputStream input) throws Exception { + try (InputStream in = input; ByteArrayOutputStream out = new ByteArrayOutputStream()) { + byte[] buffer = new byte[8192]; + int len; + while ((len = in.read(buffer)) > -1) { + out.write(buffer, 0, len); + } + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } + } + + private static String extractJsonString(String json, String key) { + String marker = "\"" + key + "\":\""; + int start = json.indexOf(marker); + if (start < 0) { + return null; + } + start += marker.length(); + int end = json.indexOf('"', start); + return end < 0 ? null : json.substring(start, end); + } + + private static String extractJsonNumber(String json, String key) { + String marker = "\"" + key + "\":"; + int start = json.indexOf(marker); + if (start < 0) { + return null; + } + start += marker.length(); + int end = start; + while (end < json.length()) { + char ch = json.charAt(end); + if ((ch < '0' || ch > '9') && ch != '-') { + break; + } + end++; + } + return json.substring(start, end); + } + + private static String workerHarnessSource(Path distDir, String appName) { + return "" + + "const fs = require('fs');\n" + + "const path = require('path');\n" + + "const vm = require('vm');\n" + + "const __messages = [];\n" + + "let __timerId = 1;\n" + + "let __now = 0;\n" + + "const __timers = [];\n" + + "global.self = global;\n" + + "global.window = global;\n" + + "global.global = global;\n" + + "Date.now = function() { return __now; };\n" + + "global.setTimeout = function(fn, millis) {\n" + + " const timer = { id: __timerId++, due: __now + Math.max(0, millis | 0), fn: fn, cleared: false };\n" + + " __timers.push(timer);\n" + + " return timer;\n" + + "};\n" + + "global.clearTimeout = function(timer) {\n" + + " if (timer) {\n" + + " timer.cleared = true;\n" + + " }\n" + + "};\n" + + "global.postMessage = function(msg) { __messages.push(msg); };\n" + + "global.importScripts = function() {\n" + + " for (const script of arguments) {\n" + + " const scriptPath = path.join(" + quoteJs(distDir.toString()) + ", String(script));\n" + + " let src = fs.readFileSync(scriptPath, 'utf8');\n" + + " if (String(script) === 'translated_app.js') {\n" + + " src += '\\nif (typeof jvm !== \"undefined\" && jvm.mainMethod) { global.__cn1ExportedMain = eval(jvm.mainMethod); }\\n';\n" + + " }\n" + + " vm.runInThisContext(src, { filename: scriptPath });\n" + + " }\n" + + "};\n" + + "importScripts('parparvm_runtime.js');\n" + + "importScripts('translated_app.js');\n" + + "const mainFn = global.__cn1ExportedMain;\n" + + "const mainThreadObject = jvm.newObject('java_lang_Thread');\n" + + "mainThreadObject.cn1_java_lang_Thread_alive = 1;\n" + + "mainThreadObject.cn1_java_lang_Thread_name = jvm.createStringLiteral('main');\n" + + "jvm.spawn(mainThreadObject, mainFn(jvm.newArray(0, 'java_lang_String', 1)));\n" + + "while (jvm.runnable.length || __timers.length) {\n" + + " if (jvm.runnable.length) {\n" + + " jvm.drain();\n" + + " continue;\n" + + " }\n" + + " __timers.sort(function(a, b) { return a.due - b.due || a.id - b.id; });\n" + + " const timer = __timers.shift();\n" + + " if (!timer || timer.cleared) {\n" + + " continue;\n" + + " }\n" + + " __now = Math.max(__now, timer.due);\n" + + " timer.fn();\n" + + "}\n" + + "const resultValue = jvm.classes[" + quoteJs(appName) + "].staticFields['result'];\n" + + "const finalMessage = __messages.length ? __messages[__messages.length - 1] : { type: 'result', result: resultValue };\n" + + "if (finalMessage.type !== 'error') {\n" + + " finalMessage.type = 'result';\n" + + " finalMessage.result = resultValue;\n" + + "}\n" + + "console.log(JSON.stringify(finalMessage));\n"; + } + + private static String generatedWorkerHarnessSource(Path distDir) { + return "" + + "const fs = require('fs');\n" + + "const path = require('path');\n" + + "const { Worker } = require('worker_threads');\n" + + "const bootstrapPath = path.join(" + quoteJs(distDir.toString()) + ", '__node_worker_bootstrap.js');\n" + + "fs.writeFileSync(bootstrapPath, `\n" + + "const fs = require('fs');\n" + + "const path = require('path');\n" + + "const vm = require('vm');\n" + + "const { parentPort, workerData } = require('worker_threads');\n" + + "global.self = global;\n" + + "global.window = global;\n" + + "global.global = global;\n" + + "global.postMessage = function(msg) { parentPort.postMessage(msg); };\n" + + "global.importScripts = function() {\n" + + " for (const script of arguments) {\n" + + " const scriptPath = path.join(workerData.distDir, String(script));\n" + + " const src = fs.readFileSync(scriptPath, 'utf8');\n" + + " vm.runInThisContext(src, { filename: scriptPath });\n" + + " }\n" + + "};\n" + + "parentPort.on('message', function(data) {\n" + + " if (typeof self.onmessage === 'function') {\n" + + " self.onmessage({ data: data });\n" + + " }\n" + + "});\n" + + "const workerSrc = fs.readFileSync(path.join(workerData.distDir, 'worker.js'), 'utf8');\n" + + "vm.runInThisContext(workerSrc, { filename: path.join(workerData.distDir, 'worker.js') });\n" + + "`);\n" + + "const worker = new Worker(bootstrapPath, { workerData: { distDir: " + quoteJs(distDir.toString()) + " } });\n" + + "let done = false;\n" + + "worker.on('message', function(msg) {\n" + + " if (done) {\n" + + " return;\n" + + " }\n" + + " if (msg && (msg.type === 'result' || msg.type === 'error')) {\n" + + " done = true;\n" + + " console.log(JSON.stringify(msg));\n" + + " worker.terminate().then(function() { process.exit(0); });\n" + + " }\n" + + "});\n" + + "worker.on('error', function(err) {\n" + + " if (done) {\n" + + " return;\n" + + " }\n" + + " done = true;\n" + + " console.log(JSON.stringify({ type: 'error', message: String(err) }));\n" + + " process.exit(1);\n" + + "});\n" + + "worker.postMessage({ type: 'start' });\n" + + "setTimeout(function() {\n" + + " if (!done) {\n" + + " console.log(JSON.stringify({ type: 'error', message: 'Timed out waiting for worker result' }));\n" + + " worker.terminate().then(function() { process.exit(1); });\n" + + " }\n" + + "}, 3000);\n"; + } + + private static String generatedWorkerHarnessSourceWithHostCallbacks(Path distDir) { + return "" + + "const fs = require('fs');\n" + + "const path = require('path');\n" + + "const { Worker } = require('worker_threads');\n" + + "const bootstrapPath = path.join(" + quoteJs(distDir.toString()) + ", '__node_worker_bootstrap_host.js');\n" + + "fs.writeFileSync(bootstrapPath, `\n" + + "const fs = require('fs');\n" + + "const path = require('path');\n" + + "const vm = require('vm');\n" + + "const { parentPort, workerData } = require('worker_threads');\n" + + "global.self = global;\n" + + "global.window = global;\n" + + "global.global = global;\n" + + "global.postMessage = function(msg) { parentPort.postMessage(msg); };\n" + + "global.importScripts = function() {\n" + + " for (const script of arguments) {\n" + + " const scriptPath = path.join(workerData.distDir, String(script));\n" + + " const src = fs.readFileSync(scriptPath, 'utf8');\n" + + " vm.runInThisContext(src, { filename: scriptPath });\n" + + " }\n" + + "};\n" + + "parentPort.on('message', function(data) {\n" + + " if (typeof self.onmessage === 'function') {\n" + + " self.onmessage({ data: data });\n" + + " }\n" + + "});\n" + + "const workerSrc = fs.readFileSync(path.join(workerData.distDir, 'worker.js'), 'utf8');\n" + + "vm.runInThisContext(workerSrc, { filename: path.join(workerData.distDir, 'worker.js') });\n" + + "`);\n" + + "const worker = new Worker(bootstrapPath, { workerData: { distDir: " + quoteJs(distDir.toString()) + " } });\n" + + "let done = false;\n" + + "worker.on('message', function(msg) {\n" + + " if (done) {\n" + + " return;\n" + + " }\n" + + " if (msg && msg.type === 'host-call') {\n" + + " if (msg.symbol === 'cn1_com_codename1_impl_platform_js_VMHost_echoInt_int_R_int') {\n" + + " worker.postMessage({ type: 'host-callback', id: msg.id, value: (msg.args[0] | 0) + 1 });\n" + + " return;\n" + + " }\n" + + " worker.postMessage({ type: 'host-callback', id: msg.id, error: true, errorMessage: 'Unexpected host call ' + msg.symbol });\n" + + " return;\n" + + " }\n" + + " if (msg && (msg.type === 'result' || msg.type === 'error')) {\n" + + " done = true;\n" + + " console.log(JSON.stringify(msg));\n" + + " worker.terminate().then(function() { process.exit(0); });\n" + + " }\n" + + "});\n" + + "worker.on('error', function(err) {\n" + + " if (done) {\n" + + " return;\n" + + " }\n" + + " done = true;\n" + + " console.log(JSON.stringify({ type: 'error', message: String(err) }));\n" + + " process.exit(1);\n" + + "});\n" + + "worker.postMessage({ type: 'start' });\n" + + "setTimeout(function() {\n" + + " if (!done) {\n" + + " console.log(JSON.stringify({ type: 'error', message: 'Timed out waiting for worker result' }));\n" + + " worker.terminate().then(function() { process.exit(1); });\n" + + " }\n" + + "}, 3000);\n"; + } + + private static String generatedWorkerHarnessSourceWithProtocolHandshake(Path distDir) { + return "" + + "const fs = require('fs');\n" + + "const path = require('path');\n" + + "const { Worker } = require('worker_threads');\n" + + "const bootstrapPath = path.join(" + quoteJs(distDir.toString()) + ", '__node_worker_bootstrap_protocol.js');\n" + + "fs.writeFileSync(bootstrapPath, `\n" + + "const fs = require('fs');\n" + + "const path = require('path');\n" + + "const vm = require('vm');\n" + + "const { parentPort, workerData } = require('worker_threads');\n" + + "global.self = global;\n" + + "global.window = global;\n" + + "global.global = global;\n" + + "global.postMessage = function(msg) { parentPort.postMessage(msg); };\n" + + "global.importScripts = function() {\n" + + " for (const script of arguments) {\n" + + " const scriptPath = path.join(workerData.distDir, String(script));\n" + + " const src = fs.readFileSync(scriptPath, 'utf8');\n" + + " vm.runInThisContext(src, { filename: scriptPath });\n" + + " }\n" + + "};\n" + + "parentPort.on('message', function(data) {\n" + + " if (typeof self.onmessage === 'function') {\n" + + " self.onmessage({ data: data });\n" + + " }\n" + + "});\n" + + "const workerSrc = fs.readFileSync(path.join(workerData.distDir, 'worker.js'), 'utf8');\n" + + "vm.runInThisContext(workerSrc, { filename: path.join(workerData.distDir, 'worker.js') });\n" + + "`);\n" + + "const worker = new Worker(bootstrapPath, { workerData: { distDir: " + quoteJs(distDir.toString()) + " } });\n" + + "let done = false;\n" + + "let protocol = null;\n" + + "worker.on('message', function(msg) {\n" + + " if (done) {\n" + + " return;\n" + + " }\n" + + " if (msg && msg.type === 'protocol') {\n" + + " protocol = msg;\n" + + " worker.postMessage({ type: protocol.messages.START });\n" + + " return;\n" + + " }\n" + + " if (msg && (msg.type === 'result' || msg.type === 'error')) {\n" + + " done = true;\n" + + " if (msg.type === 'error' || !protocol) {\n" + + " console.log(JSON.stringify(msg));\n" + + " } else {\n" + + " console.log(JSON.stringify({\n" + + " type: 'protocol-check',\n" + + " version: protocol.version,\n" + + " startType: protocol.messages.START,\n" + + " hostCallbackType: protocol.messages.HOST_CALLBACK,\n" + + " timerWakeType: protocol.messages.TIMER_WAKE,\n" + + " result: msg.result\n" + + " }));\n" + + " }\n" + + " worker.terminate().then(function() { process.exit(0); });\n" + + " }\n" + + "});\n" + + "worker.on('error', function(err) {\n" + + " if (done) {\n" + + " return;\n" + + " }\n" + + " done = true;\n" + + " console.log(JSON.stringify({ type: 'error', message: String(err) }));\n" + + " process.exit(1);\n" + + "});\n" + + "worker.postMessage({ type: 'protocol-info' });\n" + + "setTimeout(function() {\n" + + " if (!done) {\n" + + " console.log(JSON.stringify({ type: 'error', message: 'Timed out waiting for worker protocol handshake' }));\n" + + " worker.terminate().then(function() { process.exit(1); });\n" + + " }\n" + + "}, 3000);\n"; + } + + private static String generatedWorkerHarnessSourceWithEventInjection(Path distDir) { + return "" + + "const fs = require('fs');\n" + + "const path = require('path');\n" + + "const { Worker } = require('worker_threads');\n" + + "const bootstrapPath = path.join(" + quoteJs(distDir.toString()) + ", '__node_worker_bootstrap_event.js');\n" + + "fs.writeFileSync(bootstrapPath, `\n" + + "const fs = require('fs');\n" + + "const path = require('path');\n" + + "const vm = require('vm');\n" + + "const { parentPort, workerData } = require('worker_threads');\n" + + "global.self = global;\n" + + "global.window = global;\n" + + "global.global = global;\n" + + "global.postMessage = function(msg) { parentPort.postMessage(msg); };\n" + + "global.importScripts = function() {\n" + + " for (const script of arguments) {\n" + + " const scriptPath = path.join(workerData.distDir, String(script));\n" + + " const src = fs.readFileSync(scriptPath, 'utf8');\n" + + " vm.runInThisContext(src, { filename: scriptPath });\n" + + " }\n" + + "};\n" + + "parentPort.on('message', function(data) {\n" + + " if (typeof self.onmessage === 'function') {\n" + + " self.onmessage({ data: data });\n" + + " }\n" + + "});\n" + + "const workerSrc = fs.readFileSync(path.join(workerData.distDir, 'worker.js'), 'utf8');\n" + + "vm.runInThisContext(workerSrc, { filename: path.join(workerData.distDir, 'worker.js') });\n" + + "`);\n" + + "const worker = new Worker(bootstrapPath, { workerData: { distDir: " + quoteJs(distDir.toString()) + " } });\n" + + "let done = false;\n" + + "worker.on('message', function(msg) {\n" + + " if (done) {\n" + + " return;\n" + + " }\n" + + " if (msg && msg.type === 'host-call') {\n" + + " if (msg.symbol === 'cn1_com_codename1_impl_platform_js_VMHost_echoInt_int_R_int') {\n" + + " worker.postMessage({ type: 'host-callback', id: msg.id, value: (msg.args[0] | 0) + 1 });\n" + + " return;\n" + + " }\n" + + " worker.postMessage({ type: 'host-callback', id: msg.id, error: true, errorMessage: 'Unexpected host call ' + msg.symbol });\n" + + " return;\n" + + " }\n" + + " if (msg && (msg.type === 'result' || msg.type === 'error')) {\n" + + " done = true;\n" + + " console.log(JSON.stringify(msg));\n" + + " worker.terminate().then(function() { process.exit(0); });\n" + + " }\n" + + "});\n" + + "worker.on('error', function(err) {\n" + + " if (done) {\n" + + " return;\n" + + " }\n" + + " done = true;\n" + + " console.log(JSON.stringify({ type: 'error', message: String(err) }));\n" + + " process.exit(1);\n" + + "});\n" + + "worker.postMessage({ type: 'unknown-protocol-message', code: 999 });\n" + + "worker.postMessage({ type: 'event', code: 41 });\n" + + "worker.postMessage({ type: 'start' });\n" + + "setTimeout(function() {\n" + + " if (!done) {\n" + + " console.log(JSON.stringify({ type: 'error', message: 'Timed out waiting for worker event injection result' }));\n" + + " worker.terminate().then(function() { process.exit(1); });\n" + + " }\n" + + "}, 3000);\n"; + } + + private static String generatedWorkerHarnessSourceWithQueuedEvents(Path distDir) { + return "" + + "const fs = require('fs');\n" + + "const path = require('path');\n" + + "const { Worker } = require('worker_threads');\n" + + "const bootstrapPath = path.join(" + quoteJs(distDir.toString()) + ", '__node_worker_bootstrap_queue.js');\n" + + "fs.writeFileSync(bootstrapPath, `\n" + + "const fs = require('fs');\n" + + "const path = require('path');\n" + + "const vm = require('vm');\n" + + "const { parentPort, workerData } = require('worker_threads');\n" + + "global.self = global;\n" + + "global.window = global;\n" + + "global.global = global;\n" + + "global.postMessage = function(msg) { parentPort.postMessage(msg); };\n" + + "global.importScripts = function() {\n" + + " for (const script of arguments) {\n" + + " const scriptPath = path.join(workerData.distDir, String(script));\n" + + " const src = fs.readFileSync(scriptPath, 'utf8');\n" + + " vm.runInThisContext(src, { filename: scriptPath });\n" + + " }\n" + + "};\n" + + "parentPort.on('message', function(data) {\n" + + " if (typeof self.onmessage === 'function') {\n" + + " self.onmessage({ data: data });\n" + + " }\n" + + "});\n" + + "const workerSrc = fs.readFileSync(path.join(workerData.distDir, 'worker.js'), 'utf8');\n" + + "vm.runInThisContext(workerSrc, { filename: path.join(workerData.distDir, 'worker.js') });\n" + + "`);\n" + + "const worker = new Worker(bootstrapPath, { workerData: { distDir: " + quoteJs(distDir.toString()) + " } });\n" + + "let done = false;\n" + + "worker.on('message', function(msg) {\n" + + " if (done) {\n" + + " return;\n" + + " }\n" + + " if (msg && (msg.type === 'result' || msg.type === 'error')) {\n" + + " done = true;\n" + + " console.log(JSON.stringify(msg));\n" + + " worker.terminate().then(function() { process.exit(0); });\n" + + " }\n" + + "});\n" + + "worker.on('error', function(err) {\n" + + " if (done) {\n" + + " return;\n" + + " }\n" + + " done = true;\n" + + " console.log(JSON.stringify({ type: 'error', message: String(err) }));\n" + + " process.exit(1);\n" + + "});\n" + + "worker.postMessage({ type: 'event', code: 4 });\n" + + "worker.postMessage({ type: 'ui-event', code: 12 });\n" + + "worker.postMessage({ type: 'start' });\n" + + "setTimeout(function() {\n" + + " if (!done) {\n" + + " console.log(JSON.stringify({ type: 'error', message: 'Timed out waiting for queued event result' }));\n" + + " worker.terminate().then(function() { process.exit(1); });\n" + + " }\n" + + "}, 3000);\n"; + } + + private static String generatedWorkerHarnessSourceWithHostCallbackError(Path distDir) { + return "" + + "const fs = require('fs');\n" + + "const path = require('path');\n" + + "const { Worker } = require('worker_threads');\n" + + "const bootstrapPath = path.join(" + quoteJs(distDir.toString()) + ", '__node_worker_bootstrap_host_error.js');\n" + + "fs.writeFileSync(bootstrapPath, `\n" + + "const fs = require('fs');\n" + + "const path = require('path');\n" + + "const vm = require('vm');\n" + + "const { parentPort, workerData } = require('worker_threads');\n" + + "global.self = global;\n" + + "global.window = global;\n" + + "global.global = global;\n" + + "global.postMessage = function(msg) { parentPort.postMessage(msg); };\n" + + "global.importScripts = function() {\n" + + " for (const script of arguments) {\n" + + " const scriptPath = path.join(workerData.distDir, String(script));\n" + + " const src = fs.readFileSync(scriptPath, 'utf8');\n" + + " vm.runInThisContext(src, { filename: scriptPath });\n" + + " }\n" + + "};\n" + + "parentPort.on('message', function(data) {\n" + + " if (typeof self.onmessage === 'function') {\n" + + " self.onmessage({ data: data });\n" + + " }\n" + + "});\n" + + "const workerSrc = fs.readFileSync(path.join(workerData.distDir, 'worker.js'), 'utf8');\n" + + "vm.runInThisContext(workerSrc, { filename: path.join(workerData.distDir, 'worker.js') });\n" + + "`);\n" + + "const worker = new Worker(bootstrapPath, { workerData: { distDir: " + quoteJs(distDir.toString()) + " } });\n" + + "let done = false;\n" + + "worker.on('message', function(msg) {\n" + + " if (done) {\n" + + " return;\n" + + " }\n" + + " if (msg && msg.type === 'host-call') {\n" + + " worker.postMessage({ type: 'host-callback', id: msg.id, error: true, errorMessage: 'Injected host failure ' + (msg.args && msg.args.length ? msg.args[0] : '?') });\n" + + " return;\n" + + " }\n" + + " if (msg && (msg.type === 'result' || msg.type === 'error')) {\n" + + " done = true;\n" + + " console.log(JSON.stringify(msg));\n" + + " worker.terminate().then(function() { process.exit(0); });\n" + + " }\n" + + "});\n" + + "worker.on('error', function(err) {\n" + + " if (done) {\n" + + " return;\n" + + " }\n" + + " done = true;\n" + + " console.log(JSON.stringify({ type: 'error', message: String(err) }));\n" + + " process.exit(1);\n" + + "});\n" + + "worker.postMessage({ type: 'start' });\n" + + "setTimeout(function() {\n" + + " if (!done) {\n" + + " console.log(JSON.stringify({ type: 'error', message: 'Timed out waiting for host error result' }));\n" + + " worker.terminate().then(function() { process.exit(1); });\n" + + " }\n" + + "}, 3000);\n"; + } + + private static String quoteJs(String value) { + return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; + } + + private static final class WorkerRunResult { + String type; + int result; + String errorMessage; + String rawMessage; + int protocolVersion; + String protocolStartType; + String protocolHostCallbackType; + String protocolTimerWakeType; + } +} diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptTargetIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptTargetIntegrationTest.java new file mode 100644 index 0000000000..470a1f5dfa --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptTargetIntegrationTest.java @@ -0,0 +1,336 @@ +package com.codename1.tools.translator; + +import org.junit.jupiter.params.ParameterizedTest; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class JavascriptTargetIntegrationTest { + + @ParameterizedTest + @org.junit.jupiter.params.provider.MethodSource("com.codename1.tools.translator.BytecodeInstructionIntegrationTest#provideCompilerConfigs") + void generatesBrowserBundleForJavascriptTarget(CompilerHelper.CompilerConfig config) throws Exception { + Parser.cleanup(); + + Path sourceDir = Files.createTempDirectory("js-target-sources"); + Path classesDir = Files.createTempDirectory("js-target-classes"); + Path javaApiDir = Files.createTempDirectory("java-api-classes"); + + Files.write(sourceDir.resolve("JsHello.java"), loadFixture("JsHello.java").getBytes(StandardCharsets.UTF_8)); + + compileAgainstJavaApi(config, sourceDir, classesDir, javaApiDir); + + Path outputDir = Files.createTempDirectory("js-target-output"); + runJavascriptTranslator(classesDir, outputDir, "JsHello"); + + Path distDir = outputDir.resolve("dist").resolve("JsHello-js"); + assertTrue(Files.exists(distDir.resolve("index.html")), "Translator should emit a minimal host page"); + assertTrue(Files.exists(distDir.resolve("worker.js")), "Translator should emit a worker bootstrap"); + assertTrue(Files.exists(distDir.resolve("parparvm_runtime.js")), "Translator should emit a JS runtime"); + assertTrue(Files.exists(distDir.resolve("translated_app.js")), "Translator should emit translated classes"); + + String translatedApp = new String(Files.readAllBytes(distDir.resolve("translated_app.js")), StandardCharsets.UTF_8); + String runtime = new String(Files.readAllBytes(distDir.resolve("parparvm_runtime.js")), StandardCharsets.UTF_8); + String worker = new String(Files.readAllBytes(distDir.resolve("worker.js")), StandardCharsets.UTF_8); + + assertTrue(translatedApp.contains("function*") && translatedApp.contains("JsHello"), + "Main class should contribute translated JS generator functions"); + assertTrue(translatedApp.contains("jvm.setMain(\"JsHello\""), + "Bundle should register the translated main entrypoint"); + assertTrue(runtime.contains("cn1_java_lang_Thread_start") || runtime.contains("cn1_java_lang_Thread_start__"), + "Runtime should provide JS native implementations for thread start"); + assertTrue(runtime.contains("cn1_java_lang_Object_wait_long_int") || runtime.contains("cn1_java_lang_Object_wait___long_int"), + "Runtime should provide JS native implementations for wait()"); + assertTrue(!translatedApp.contains("Missing javascript native method cn1_java_lang_Object_wait_long_int"), + "Translated bundle should not emit generic fallback stubs for runtime-implemented natives"); + assertTrue(!translatedApp.contains("Missing javascript native method cn1_java_util_Locale_getOSLanguage_R_java_lang_String"), + "Translated bundle should not emit generic fallback stubs for Locale natives"); + assertTrue(!translatedApp.contains("Missing javascript native method cn1_java_util_TimeZone_getTimezoneId_R_java_lang_String"), + "Translated bundle should not emit generic fallback stubs for TimeZone natives"); + assertTrue(!translatedApp.contains("Missing javascript native method cn1_java_text_DateFormat_format_java_util_Date_java_lang_StringBuffer_R_java_lang_String"), + "Translated bundle should not emit generic fallback stubs for DateFormat natives"); + assertTrue(!translatedApp.contains("__args.unshift(stack.pop())"), + "Translated invoke paths should avoid array unshift-based argument packing"); + assertTrue(!translatedApp.contains("cn1_java_io_File_") + || translatedApp.contains("java.io.File native filesystem access is not supported in javascript backend"), + "Unsupported filesystem natives should fail with an explicit JS-mode message when translated"); + assertTrue(worker.contains("importScripts('parparvm_runtime.js');"), + "Worker bootstrap should load the runtime first"); + } + + @ParameterizedTest + @org.junit.jupiter.params.provider.MethodSource("com.codename1.tools.translator.BytecodeInstructionIntegrationTest#provideCompilerConfigs") + void generatesWaitNotifyFriendlyJavascriptBundle(CompilerHelper.CompilerConfig config) throws Exception { + Parser.cleanup(); + + Path sourceDir = Files.createTempDirectory("js-thread-sources"); + Path classesDir = Files.createTempDirectory("js-thread-classes"); + Path javaApiDir = Files.createTempDirectory("java-api-classes"); + + Files.write(sourceDir.resolve("JsThreadingApp.java"), loadFixture("JsThreadingApp.java").getBytes(StandardCharsets.UTF_8)); + + compileAgainstJavaApi(config, sourceDir, classesDir, javaApiDir); + + Path outputDir = Files.createTempDirectory("js-thread-output"); + runJavascriptTranslator(classesDir, outputDir, "JsThreadingApp"); + + Path distDir = outputDir.resolve("dist").resolve("JsThreadingApp-js"); + String translatedApp = new String(Files.readAllBytes(distDir.resolve("translated_app.js")), StandardCharsets.UTF_8); + String runtime = new String(Files.readAllBytes(distDir.resolve("parparvm_runtime.js")), StandardCharsets.UTF_8); + + assertTrue(translatedApp.contains("jvm.setMain(\"JsThreadingApp\""), + "Threaded app should register a translated main entrypoint"); + assertTrue(translatedApp.contains("waitForSignal"), + "Inner-class wait loop should be present in translated output"); + assertTrue(runtime.contains("waitOn(thread, obj, timeout)"), + "Runtime should include cooperative monitor wait support"); + assertTrue(runtime.contains("notifyAll(obj)"), + "Runtime should include cooperative notifyAll support"); + } + + @ParameterizedTest + @org.junit.jupiter.params.provider.MethodSource("com.codename1.tools.translator.BytecodeInstructionIntegrationTest#provideCompilerConfigs") + void representativeJavascriptBundleHasNoUncategorizedNativeFallbacks(CompilerHelper.CompilerConfig config) throws Exception { + Parser.cleanup(); + + Path sourceDir = Files.createTempDirectory("js-fallback-sources"); + Path classesDir = Files.createTempDirectory("js-fallback-classes"); + Path javaApiDir = Files.createTempDirectory("java-api-fallback-classes"); + + Files.write(sourceDir.resolve("JsLocaleTimeZoneApp.java"), loadFixture("JsLocaleTimeZoneApp.java").getBytes(StandardCharsets.UTF_8)); + + compileAgainstJavaApi(config, sourceDir, classesDir, javaApiDir); + + Path outputDir = Files.createTempDirectory("js-fallback-output"); + runJavascriptTranslator(classesDir, outputDir, "JsLocaleTimeZoneApp"); + + Path distDir = outputDir.resolve("dist").resolve("JsLocaleTimeZoneApp-js"); + String translatedApp = new String(Files.readAllBytes(distDir.resolve("translated_app.js")), StandardCharsets.UTF_8); + + assertTrue(!translatedApp.contains("Missing javascript native method "), + "Representative JS bundles should not retain uncategorized native fallback stubs"); + } + + @ParameterizedTest + @org.junit.jupiter.params.provider.MethodSource("com.codename1.tools.translator.BytecodeInstructionIntegrationTest#provideCompilerConfigs") + void broaderJavaApiBundleHasNoUncategorizedNativeFallbacks(CompilerHelper.CompilerConfig config) throws Exception { + Parser.cleanup(); + + Path sourceDir = Files.createTempDirectory("js-javaapi-sources"); + Path classesDir = Files.createTempDirectory("js-javaapi-classes"); + Path javaApiDir = Files.createTempDirectory("java-api-javaapi-classes"); + + Files.write(sourceDir.resolve("JsJavaApiCoverageApp.java"), loadFixture("JsJavaApiCoverageApp.java").getBytes(StandardCharsets.UTF_8)); + + compileAgainstJavaApi(config, sourceDir, classesDir, javaApiDir); + + Path outputDir = Files.createTempDirectory("js-javaapi-output"); + runJavascriptTranslator(classesDir, outputDir, "JsJavaApiCoverageApp"); + + Path distDir = outputDir.resolve("dist").resolve("JsJavaApiCoverageApp-js"); + String translatedApp = new String(Files.readAllBytes(distDir.resolve("translated_app.js")), StandardCharsets.UTF_8); + + assertTrue(!translatedApp.contains("Missing javascript native method "), + "Broader JavaAPI JS bundles should not retain uncategorized native fallback stubs"); + } + + @ParameterizedTest + @org.junit.jupiter.params.provider.MethodSource("com.codename1.tools.translator.BytecodeInstructionIntegrationTest#provideCompilerConfigs") + void hostHookNativesGenerateVmHostCalls(CompilerHelper.CompilerConfig config) throws Exception { + Parser.cleanup(); + + Path sourceDir = Files.createTempDirectory("js-host-hook-sources"); + Path classesDir = Files.createTempDirectory("js-host-hook-classes"); + Path javaApiDir = Files.createTempDirectory("java-api-host-hook-classes"); + + Path vmHostDir = sourceDir.resolve("com").resolve("codename1").resolve("impl").resolve("platform").resolve("js"); + Files.createDirectories(vmHostDir); + Files.write(vmHostDir.resolve("VMHost.java"), loadFixture("com/codename1/impl/platform/js/VMHost.java").getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("JsHostCallbackApp.java"), loadFixture("JsHostCallbackApp.java").getBytes(StandardCharsets.UTF_8)); + + compileAgainstJavaApi(config, sourceDir, classesDir, javaApiDir); + + Path outputDir = Files.createTempDirectory("js-host-hook-output"); + runJavascriptTranslator(classesDir, outputDir, "JsHostCallbackApp"); + + Path distDir = outputDir.resolve("dist").resolve("JsHostCallbackApp-js"); + String translatedApp = new String(Files.readAllBytes(distDir.resolve("translated_app.js")), StandardCharsets.UTF_8); + + assertTrue(translatedApp.contains("jvm.invokeHostNative(\"cn1_com_codename1_impl_platform_js_VMHost_echoInt_int_R_int\""), + "Host-hook natives should compile to VM host-call stubs"); + assertTrue(!translatedApp.contains("Missing javascript native method cn1_com_codename1_impl_platform_js_VMHost_echoInt_int_R_int"), + "Host-hook natives should not compile to generic missing-native stubs"); + } + + @ParameterizedTest + @org.junit.jupiter.params.provider.MethodSource("com.codename1.tools.translator.BytecodeInstructionIntegrationTest#provideCompilerConfigs") + void simpleStraightLineMethodsLowerToLocalsInsteadOfInterpreterLoop(CompilerHelper.CompilerConfig config) throws Exception { + Parser.cleanup(); + + Path sourceDir = Files.createTempDirectory("js-straight-line-sources"); + Path classesDir = Files.createTempDirectory("js-straight-line-classes"); + Path javaApiDir = Files.createTempDirectory("java-api-straight-line-classes"); + + Files.write(sourceDir.resolve("JsStraightLine.java"), loadFixture("JsStraightLine.java").getBytes(StandardCharsets.UTF_8)); + + compileAgainstJavaApi(config, sourceDir, classesDir, javaApiDir); + + Path outputDir = Files.createTempDirectory("js-straight-line-output"); + runJavascriptTranslator(classesDir, outputDir, "JsStraightLine"); + + Path distDir = outputDir.resolve("dist").resolve("JsStraightLine-js"); + String translatedApp = new String(Files.readAllBytes(distDir.resolve("translated_app.js")), StandardCharsets.UTF_8); + + String marker = "function* cn1_JsStraightLine_add_int_int_R_int(__cn1Arg1, __cn1Arg2){"; + int start = translatedApp.indexOf(marker); + assertTrue(start >= 0, "Straight-line fixture should emit the add() method"); + int end = translatedApp.indexOf("\n}\n", start); + assertTrue(end > start, "Straight-line fixture should have a bounded method body"); + String methodBody = translatedApp.substring(start, end); + + assertTrue(methodBody.contains("let l0 = __cn1Arg1;") && methodBody.contains("let l1 = __cn1Arg2;"), + "Straight-line lowering should use direct local variables for arguments"); + assertTrue(!methodBody.contains("stack["), + "Straight-line lowering should avoid stack-array indexing"); + assertTrue(!methodBody.contains("const locals = new Array") && !methodBody.contains("const stack = []") && !methodBody.contains("let pc = 0"), + "Straight-line lowering should avoid the interpreter locals/stack/pc loop"); + } + + @ParameterizedTest + @org.junit.jupiter.params.provider.MethodSource("com.codename1.tools.translator.BytecodeInstructionIntegrationTest#provideCompilerConfigs") + void repeatedStaticAccessesOnlyEmitOneClassInitCheckInStraightLineMode(CompilerHelper.CompilerConfig config) throws Exception { + Parser.cleanup(); + + Path sourceDir = Files.createTempDirectory("js-static-access-sources"); + Path classesDir = Files.createTempDirectory("js-static-access-classes"); + Path javaApiDir = Files.createTempDirectory("java-api-static-access-classes"); + + Files.write(sourceDir.resolve("JsStaticAccess.java"), loadFixture("JsStaticAccess.java").getBytes(StandardCharsets.UTF_8)); + + compileAgainstJavaApi(config, sourceDir, classesDir, javaApiDir); + + Path outputDir = Files.createTempDirectory("js-static-access-output"); + runJavascriptTranslator(classesDir, outputDir, "JsStaticAccess"); + + Path distDir = outputDir.resolve("dist").resolve("JsStaticAccess-js"); + String translatedApp = new String(Files.readAllBytes(distDir.resolve("translated_app.js")), StandardCharsets.UTF_8); + + String marker = "function* cn1_JsStaticAccess_twice_R_int(){"; + int start = translatedApp.indexOf(marker); + assertTrue(start >= 0, "Static access fixture should emit the twice() method"); + int end = translatedApp.indexOf("\n}\n", start); + assertTrue(end > start, "Static access fixture should have a bounded method body"); + String methodBody = translatedApp.substring(start, end); + + String initCheck = "jvm.ensureClassInitialized(\"JsStaticAccess\");"; + assertEquals(methodBody.indexOf(initCheck), methodBody.lastIndexOf(initCheck), + "Repeated static field access should only emit one class-init check in straight-line mode"); + } + + static void compileAgainstJavaApi(CompilerHelper.CompilerConfig config, Path sourceDir, Path classesDir, Path javaApiDir) throws Exception { + assertTrue(CompilerHelper.isJavaApiCompatible(config), + "JDK " + config.jdkVersion + " must target matching bytecode level for JavaAPI"); + CompilerHelper.compileJavaAPI(javaApiDir, config); + + List sources = new ArrayList(); + try (Stream paths = Files.walk(sourceDir)) { + paths.filter(path -> path.toString().endsWith(".java")).forEach(path -> sources.add(path.toString())); + } + + List compileArgs = new ArrayList(); + compileArgs.add("-source"); + compileArgs.add(config.targetVersion); + compileArgs.add("-target"); + compileArgs.add(config.targetVersion); + if (CompilerHelper.useClasspath(config)) { + compileArgs.add("-classpath"); + compileArgs.add(javaApiDir.toString()); + } else { + compileArgs.add("-bootclasspath"); + compileArgs.add(javaApiDir.toString()); + compileArgs.add("-Xlint:-options"); + } + compileArgs.add("-d"); + compileArgs.add(classesDir.toString()); + compileArgs.addAll(sources); + + int compileResult = CompilerHelper.compile(config.jdkHome, compileArgs); + assertEquals(0, compileResult, "Compilation failed for javascript target fixture with " + config); + + CompilerHelper.copyDirectory(javaApiDir, classesDir); + } + + static void runJavascriptTranslator(Path classesDir, Path outputDir, String appName) throws Exception { + Class translatorClass = ByteCodeTranslator.class; + try { + java.lang.reflect.Field verboseField = translatorClass.getField("verbose"); + boolean originalVerbose = verboseField.getBoolean(null); + verboseField.setBoolean(null, false); + Method main = translatorClass.getMethod("main", String[].class); + String[] args = new String[]{ + "javascript", + classesDir.toString(), + outputDir.toString(), + appName, + "com.example.javascript", + appName, + "1.0", + "ios", + "none" + }; + try { + main.invoke(null, (Object) args); + } catch (InvocationTargetException ite) { + Throwable cause = ite.getCause() != null ? ite.getCause() : ite; + if (cause instanceof Exception) { + throw (Exception) cause; + } + throw new RuntimeException(cause); + } finally { + verboseField.setBoolean(null, originalVerbose); + } + } finally { + Parser.cleanup(); + } + } + + static String loadFixture(String name) throws Exception { + InputStream input = JavascriptTargetIntegrationTest.class.getResourceAsStream("/com/codename1/tools/translator/" + name); + if (input != null) { + try { + byte[] buffer = new byte[8192]; + StringBuilder out = new StringBuilder(); + int len; + while ((len = input.read(buffer)) > -1) { + out.append(new String(buffer, 0, len, StandardCharsets.UTF_8)); + } + return out.toString(); + } finally { + input.close(); + } + } + + Path modulePath = Paths.get("src", "test", "resources", "com", "codename1", "tools", "translator", name); + if (Files.exists(modulePath)) { + return new String(Files.readAllBytes(modulePath), StandardCharsets.UTF_8); + } + + Path repoPath = Paths.get("vm", "tests", "src", "test", "resources", "com", "codename1", "tools", "translator", name); + if (Files.exists(repoPath)) { + return new String(Files.readAllBytes(repoPath), StandardCharsets.UTF_8); + } + + throw new IllegalStateException("Missing javascript test fixture " + name); + } +} diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/LockIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/LockIntegrationTest.java index bf196cd71c..c593b623ef 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/LockIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/LockIntegrationTest.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -28,7 +29,9 @@ void verifiesLockAndReentrantLockBehavior(CompilerHelper.CompilerConfig config) // 2. Compile Test App against JavaAPI List sources = new ArrayList<>(); - Files.walk(sourceDir).filter(p -> p.toString().endsWith(".java")).forEach(p -> sources.add(p.toString())); + try (Stream paths = Files.walk(sourceDir)) { + paths.filter(p -> p.toString().endsWith(".java")).forEach(p -> sources.add(p.toString())); + } List compileArgs = new ArrayList<>(); assertTrue(CompilerHelper.isJavaApiCompatible(config), diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/ReadWriteLockIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/ReadWriteLockIntegrationTest.java index 6c3b31d3dd..edd8b2083d 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/ReadWriteLockIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/ReadWriteLockIntegrationTest.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -28,7 +29,9 @@ void verifiesReadWriteLockBehavior(CompilerHelper.CompilerConfig config) throws // 2. Compile Test App against JavaAPI List sources = new ArrayList<>(); - Files.walk(sourceDir).filter(p -> p.toString().endsWith(".java")).forEach(p -> sources.add(p.toString())); + try (Stream paths = Files.walk(sourceDir)) { + paths.filter(p -> p.toString().endsWith(".java")).forEach(p -> sources.add(p.toString())); + } List compileArgs = new ArrayList<>(); assertTrue(CompilerHelper.isJavaApiCompatible(config), diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/StampedLockIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/StampedLockIntegrationTest.java index 3542197289..b1c4f0ac0c 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/StampedLockIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/StampedLockIntegrationTest.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -28,7 +29,9 @@ void verifiesStampedLockBehavior(CompilerHelper.CompilerConfig config) throws Ex // 2. Compile Test App against JavaAPI List sources = new ArrayList<>(); - Files.walk(sourceDir).filter(p -> p.toString().endsWith(".java")).forEach(p -> sources.add(p.toString())); + try (Stream paths = Files.walk(sourceDir)) { + paths.filter(p -> p.toString().endsWith(".java")).forEach(p -> sources.add(p.toString())); + } List compileArgs = new ArrayList<>(); assertTrue(CompilerHelper.isJavaApiCompatible(config), diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/JsArrayCovarianceApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/JsArrayCovarianceApp.java new file mode 100644 index 0000000000..f0cf3942f3 --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/JsArrayCovarianceApp.java @@ -0,0 +1,45 @@ +public class JsArrayCovarianceApp { + public static int result; + + static class Animal { + } + + static class Dog extends Animal { + } + + public static void main(String[] args) { + Dog[] dogs = new Dog[1]; + Dog[][] dogGrid = new Dog[1][]; + dogGrid[0] = new Dog[1]; + int score = 0; + + if (dogs instanceof Dog[]) { + score |= 1; + } + if (dogs instanceof Animal[]) { + score |= 2; + } + if (dogs instanceof Object[]) { + score |= 4; + } + if (dogGrid instanceof Dog[][]) { + score |= 8; + } + if (dogGrid instanceof Animal[][]) { + score |= 16; + } + if (dogGrid instanceof Object[]) { + score |= 32; + } + if (dogGrid[0] instanceof Animal[]) { + score |= 64; + } + if (((Animal[]) dogs).length == 1) { + score |= 128; + } + if (((Object[]) dogGrid).length == 1) { + score |= 256; + } + result = score; + } +} diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/JsCodenameOneCoreSliceApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/JsCodenameOneCoreSliceApp.java new file mode 100644 index 0000000000..953f6db419 --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/JsCodenameOneCoreSliceApp.java @@ -0,0 +1,37 @@ +import com.codename1.io.JSONParser; +import com.codename1.util.StringUtil; +import com.codename1.util.regex.RE; + +import java.io.StringReader; +import java.util.List; +import java.util.Map; + +public class JsCodenameOneCoreSliceApp { + public static int result; + + public static void main(String[] args) throws Exception { + int mask = 0; + + List tokens = StringUtil.tokenize("alpha,beta,gamma", ','); + if (tokens.size() == 3 && "beta".equals(tokens.get(1))) { + mask |= 1; + } + + if ("alpha-beta-gamma".equals(StringUtil.join(tokens, "-"))) { + mask |= 2; + } + + Map parsed = new JSONParser().parseJSON(new StringReader("{\"ok\":true,\"n\":7}")); + if (parsed != null && parsed.containsKey("ok") && parsed.containsKey("n")) { + mask |= 4; + } + + RE regex = new RE("cn1.*vm"); + if (regex.match("cn1-js-vm")) { + mask |= 8; + } + + result = mask; + System.exit(mask); + } +} diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/JsHello.java b/vm/tests/src/test/resources/com/codename1/tools/translator/JsHello.java new file mode 100644 index 0000000000..5408f35032 --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/JsHello.java @@ -0,0 +1,6 @@ +public class JsHello { + private static native void report(String msg); + public static void main(String[] args) { + report("Hello JS"); + } +} diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/JsHostCallbackApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/JsHostCallbackApp.java new file mode 100644 index 0000000000..469597f014 --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/JsHostCallbackApp.java @@ -0,0 +1,7 @@ +import com.codename1.impl.platform.js.VMHost; + +public class JsHostCallbackApp { + public static void main(String[] args) { + System.exit(VMHost.echoInt(41)); + } +} diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/JsHostCallbackErrorApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/JsHostCallbackErrorApp.java new file mode 100644 index 0000000000..cac9eb9fbb --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/JsHostCallbackErrorApp.java @@ -0,0 +1,7 @@ +import com.codename1.impl.platform.js.VMHost; + +public class JsHostCallbackErrorApp { + public static void main(String[] args) { + System.exit(VMHost.echoInt(7)); + } +} diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/JsHostEventApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/JsHostEventApp.java new file mode 100644 index 0000000000..0c682e1a92 --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/JsHostEventApp.java @@ -0,0 +1,9 @@ +import com.codename1.impl.platform.js.VMHost; + +public class JsHostEventApp { + public static void main(String[] args) { + int eventCode = VMHost.getLastEventCode(); + int echoed = VMHost.echoInt(eventCode); + System.exit((eventCode * 100) + echoed); + } +} diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/JsHostEventQueueApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/JsHostEventQueueApp.java new file mode 100644 index 0000000000..6e2a301811 --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/JsHostEventQueueApp.java @@ -0,0 +1,10 @@ +import com.codename1.impl.platform.js.VMHost; + +public class JsHostEventQueueApp { + public static void main(String[] args) { + int first = VMHost.pollEventCode(); + int second = VMHost.pollEventCode(); + int none = VMHost.pollEventCode(); + System.exit((first * 1000) + (second * 10) + (none + 1)); + } +} diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/JsJavaApiCoverageApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/JsJavaApiCoverageApp.java new file mode 100644 index 0000000000..46b0c42dd0 --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/JsJavaApiCoverageApp.java @@ -0,0 +1,66 @@ +import java.lang.reflect.Array; +import java.util.HashMap; + +public class JsJavaApiCoverageApp { + enum Mode { + ALPHA, + BETA + } + + static int result; + + public static void main(String[] args) throws Exception { + int mask = 0; + + HashMap map = new HashMap(); + map.put("key", "value"); + if ("value".equals(map.get("key"))) { + mask |= 1; + } + + Mode mode = Enum.valueOf(Mode.class, "BETA"); + if (mode == Mode.BETA) { + mask |= 2; + } + + Object reflectedArray = Array.newInstance(String.class, 2); + ((String[]) reflectedArray)[0] = "cn1"; + ((String[]) reflectedArray)[1] = "vm"; + if ("[Ljava.lang.String;".equals(reflectedArray.getClass().getName())) { + mask |= 4; + } + + if (Class.forName("java.lang.String") == String.class) { + mask |= 8; + } + + String formatted = String.format("%s-%d", "cn1", Integer.valueOf(7)); + if ("cn1-7".equals(formatted)) { + mask |= 16; + } + + int[] src = new int[] {1, 2, 3}; + int[] dst = new int[3]; + System.arraycopy(src, 0, dst, 0, src.length); + if (dst[2] == 3) { + mask |= 32; + } + + try { + throw new IllegalStateException("expected"); + } catch (RuntimeException ex) { + if ("expected".equals(ex.getMessage())) { + mask |= 64; + } + } + + StringBuilder builder = new StringBuilder(); + builder.append('o').append("k"); + if ("ok".equals(builder.toString())) { + mask |= 128; + } + + result = mask; + System.exit(mask); + } +} diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/JsLocaleTimeZoneApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/JsLocaleTimeZoneApp.java new file mode 100644 index 0000000000..7d355b4b50 --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/JsLocaleTimeZoneApp.java @@ -0,0 +1,55 @@ +import java.text.DateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +public class JsLocaleTimeZoneApp { + public static int result; + + public static void main(String[] args) { + int score = 0; + + Locale locale = Locale.getDefault(); + if (locale != null) { + score |= 1; + } + if (locale != null && locale.getLanguage() != null && locale.getLanguage().length() > 0) { + score |= 2; + } + if (locale != null && locale.getCountry() != null && locale.getCountry().length() > 0) { + score |= 4; + } + + TimeZone timeZone = TimeZone.getDefault(); + if (timeZone != null && timeZone.getID() != null && timeZone.getID().length() > 0) { + score |= 8; + } + String[] ids = TimeZone.getAvailableIDs(); + if (ids != null && ids.length >= 1) { + score |= 16; + } + + int rawOffset = timeZone.getRawOffset(); + if (rawOffset >= -43200000 && rawOffset <= 50400000) { + score |= 32; + } + + int offset = timeZone.getOffset(1, 2024, 0, 15, 2, 12 * 60 * 60 * 1000); + if (offset >= -43200000 && offset <= 50400000) { + score |= 64; + } + + Date sample = new Date(1704067200000L); + String formatted = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(sample); + if (formatted != null && formatted.length() > 0) { + score |= 128; + } + + String formattedDate = DateFormat.getDateInstance(DateFormat.SHORT).format(sample); + if (formattedDate != null && formattedDate.length() > 0) { + score |= 256; + } + + result = score; + } +} diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/JsStaticAccess.java b/vm/tests/src/test/resources/com/codename1/tools/translator/JsStaticAccess.java new file mode 100644 index 0000000000..3cfd769195 --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/JsStaticAccess.java @@ -0,0 +1,11 @@ +public class JsStaticAccess { + static int value = 7; + + static int twice() { + return value + value; + } + + public static void main(String[] args) { + System.exit(twice()); + } +} diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/JsStraightLine.java b/vm/tests/src/test/resources/com/codename1/tools/translator/JsStraightLine.java new file mode 100644 index 0000000000..a7d4b4ab58 --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/JsStraightLine.java @@ -0,0 +1,10 @@ +public class JsStraightLine { + static int add(int a, int b) { + int c = a + b; + return c * 2; + } + + public static void main(String[] args) { + System.exit(add(20, 1)); + } +} diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/JsThreadSemanticsApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/JsThreadSemanticsApp.java new file mode 100644 index 0000000000..6f3fc9e78e --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/JsThreadSemanticsApp.java @@ -0,0 +1,107 @@ +public class JsThreadSemanticsApp { + static final Object LOCK = new Object(); + static final Object INTERRUPT_LOCK = new Object(); + static volatile boolean ready; + public static volatile int result; + + static class WaitingWorker extends Thread { + public void run() { + synchronized (LOCK) { + result |= 1; + while (!ready) { + try { + LOCK.wait(1000); + } catch (InterruptedException err) { + result |= 2; + return; + } + } + result |= 4; + } + } + } + + static class SleepWorker extends Thread { + public void run() { + try { + Thread.sleep(5); + result |= 8; + } catch (InterruptedException err) { + result |= 16; + } + } + } + + static class InterruptSleepWorker extends Thread { + public void run() { + try { + Thread.sleep(1000); + result |= 32; + } catch (InterruptedException err) { + result |= 64; + if (!Thread.interrupted()) { + result |= 128; + } + if (!isInterrupted()) { + result |= 256; + } + } + } + } + + static class InterruptWaitWorker extends Thread { + public void run() { + synchronized (INTERRUPT_LOCK) { + result |= 512; + try { + INTERRUPT_LOCK.wait(1000); + } catch (InterruptedException err) { + result |= 1024; + } + } + } + } + + public static void main(String[] args) throws Exception { + WaitingWorker waiting = new WaitingWorker(); + waiting.start(); + while ((result & 1) == 0) { + Thread.sleep(1); + } + synchronized (LOCK) { + ready = true; + LOCK.notifyAll(); + } + waiting.join(); + if (!waiting.isAlive()) { + result |= 2048; + } + + SleepWorker sleeper = new SleepWorker(); + sleeper.start(); + sleeper.join(); + if (!sleeper.isAlive()) { + result |= 4096; + } + + InterruptSleepWorker interruptedSleep = new InterruptSleepWorker(); + interruptedSleep.start(); + Thread.sleep(1); + interruptedSleep.interrupt(); + interruptedSleep.join(); + if (!interruptedSleep.isAlive()) { + result |= 8192; + } + + InterruptWaitWorker interruptedWait = new InterruptWaitWorker(); + interruptedWait.start(); + while ((result & 512) == 0) { + Thread.sleep(1); + } + interruptedWait.interrupt(); + interruptedWait.join(); + if (!interruptedWait.isAlive()) { + result |= 16384; + } + } +} diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/JsThreadingApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/JsThreadingApp.java new file mode 100644 index 0000000000..5da0ecfb49 --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/JsThreadingApp.java @@ -0,0 +1,27 @@ +public class JsThreadingApp { + static class Shared { + private boolean ready; + synchronized void signal() { + ready = true; + notifyAll(); + } + synchronized void waitForSignal() { + while (!ready) { + try { + wait(); + } catch (InterruptedException ex) { + } + } + } + } + public static void main(String[] args) { + final Shared shared = new Shared(); + Thread worker = new Thread(new Runnable() { + public void run() { + shared.waitForSignal(); + } + }); + worker.start(); + shared.signal(); + } +} diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/JsWorkerProtocolApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/JsWorkerProtocolApp.java new file mode 100644 index 0000000000..21ce39ee43 --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/JsWorkerProtocolApp.java @@ -0,0 +1,33 @@ +public class JsWorkerProtocolApp { + static final Object LOCK = new Object(); + static boolean ready; + + static class Worker extends Thread { + public void run() { + synchronized (LOCK) { + while (!ready) { + try { + LOCK.wait(1000); + } catch (InterruptedException err) { + System.exit(900); + return; + } + } + } + System.exit(321); + } + } + + public static void main(String[] args) throws Exception { + Worker worker = new Worker(); + worker.start(); + while (!worker.isAlive()) { + Thread.sleep(1); + } + synchronized (LOCK) { + ready = true; + LOCK.notifyAll(); + } + worker.join(); + } +} diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/com/codename1/impl/platform/js/VMHost.java b/vm/tests/src/test/resources/com/codename1/tools/translator/com/codename1/impl/platform/js/VMHost.java new file mode 100644 index 0000000000..49fdbb5fb0 --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/com/codename1/impl/platform/js/VMHost.java @@ -0,0 +1,12 @@ +package com.codename1.impl.platform.js; + +public final class VMHost { + private VMHost() { + } + + public static native int echoInt(int value); + + public static native int getLastEventCode(); + + public static native int pollEventCode(); +}