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..b32ee114e2 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; } @@ -163,6 +170,8 @@ public static void main(String[] args) throws Exception { 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 +198,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 +262,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..a18de30062 --- /dev/null +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java @@ -0,0 +1,918 @@ +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.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.List; +import java.util.Map; +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"); + } + 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 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); + if (JavascriptNativeRegistry.hasRuntimeImplementation(jsMethodName)) { + 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(") { throw new Error(\""); + if (reason == null) { + out.append("Missing javascript native method ").append(jsMethodName); + } else { + out.append(JavascriptNameUtil.escapeJs(reason)); + } + 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; + } + throw new IllegalArgumentException("Unsupported instruction type in javascript output: " + + instruction.getClass().getName() + " for " + method.getMethodIdentifier()); + } + + 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.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.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.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.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"); + out.append(" const __args = [];\n"); + for (int i = argCount - 1; i >= 0; i--) { + out.append(" __args.unshift(stack.pop());\n"); + } + out.append(" const __target = stack.pop();\n"); + out.append(" const __class = jvm.classes[__target.__class];\n"); + out.append(" const __method = (__class && __class.methods && __class.methods[\"").append(methodId) + .append("\"]) || 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"); + out.append(" const __args = [];\n"); + for (int i = argCount - 1; i >= 0; i--) { + out.append(" __args.unshift(stack.pop());\n"); + } + 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("__args[").append(i).append("]"); + } + } +} 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..cf24f570de --- /dev/null +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptNativeRegistry.java @@ -0,0 +1,122 @@ +package com.codename1.tools.translator; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +final class JavascriptNativeRegistry { + 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_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_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_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" + )); + + private JavascriptNativeRegistry() { + } + + static boolean hasRuntimeImplementation(String symbol) { + return RUNTIME_IMPLEMENTED.contains(symbol); + } + + 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..8a187a58bc 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 { @@ -437,16 +438,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 +635,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/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..c2f553b72e --- /dev/null +++ b/vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js @@ -0,0 +1,1036 @@ +(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 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, + currentThread: null, + runnable: [], + threads: [], + mainClass: null, + mainMethod: null, + 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; + }, + 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: "log", message: message }); + }, + finish(result) { + global.postMessage({ type: "result", result: result }); + }, + fail(error) { + global.postMessage({ type: "error", message: "" + error, stack: error && error.stack ? error.stack : null }); + }, + 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; + const 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; + } + 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; + }, + handleMessage() {} +}; + +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 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 result = jvm.toNativeString(format).replace(/%[%sdifc]/g, function(token) { + if (token === "%%") { + return "%"; + } + const value = args[index++]; + if (token === "%c") { + return String.fromCharCode(value | 0); + } + return jvm.toNativeString(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_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_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_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; +}); +})(self); diff --git a/vm/ByteCodeTranslator/src/javascript/worker.js b/vm/ByteCodeTranslator/src/javascript/worker.js new file mode 100644 index 0000000000..f0324094a0 --- /dev/null +++ b/vm/ByteCodeTranslator/src/javascript/worker.js @@ -0,0 +1,13 @@ +self.window = self; +self.global = self; +/*__IMPORTS__*/ +self.onmessage = function(event) { + if (!event || !event.data) { + return; + } + if (event.data.type === 'start') { + jvm.start(); + } else if (event.data.type === 'ui-event') { + jvm.handleMessage(event.data); + } +}; 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..493074c790 --- /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("const __class = jvm.classes[__target.__class];") + && translatedApp.contains("__class.methods && __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..ffb29e4912 --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptRuntimeSemanticsTest.java @@ -0,0 +1,293 @@ +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 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"); + } + + 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 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 quoteJs(String value) { + return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; + } + + private static final class WorkerRunResult { + String type; + int result; + String errorMessage; + String rawMessage; + } +} 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..70fa8256ae --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptTargetIntegrationTest.java @@ -0,0 +1,203 @@ +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.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +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("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"); + } + + 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(); + Files.walk(sourceDir).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(name); + if (input == null) { + throw new IllegalStateException("Missing javascript test fixture " + name); + } + 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(); + } + } +} 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/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/JsLocaleTimeZoneApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/JsLocaleTimeZoneApp.java new file mode 100644 index 0000000000..d6f15c96e7 --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/JsLocaleTimeZoneApp.java @@ -0,0 +1,56 @@ +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; + } + + StringBuffer buffer = new StringBuffer(); + String formattedDate = DateFormat.getDateInstance(DateFormat.SHORT).format(sample, buffer); + if (formattedDate != null && formattedDate.length() > 0) { + score |= 256; + } + + result = score; + } +} 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(); + } +}