diff --git a/teavm-jso-impl/src/main/java/org/teavm/jso/plugin/AstWriter.java b/teavm-jso-impl/src/main/java/org/teavm/jso/plugin/AstWriter.java index 8667865cf..1e4dabdf2 100644 --- a/teavm-jso-impl/src/main/java/org/teavm/jso/plugin/AstWriter.java +++ b/teavm-jso-impl/src/main/java/org/teavm/jso/plugin/AstWriter.java @@ -16,7 +16,11 @@ package org.teavm.jso.plugin; import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import org.mozilla.javascript.Node; import org.mozilla.javascript.ScriptRuntime; import org.mozilla.javascript.Token; @@ -24,8 +28,10 @@ import org.mozilla.javascript.ast.ArrayComprehension; import org.mozilla.javascript.ast.ArrayComprehensionLoop; import org.mozilla.javascript.ast.ArrayLiteral; import org.mozilla.javascript.ast.AstNode; +import org.mozilla.javascript.ast.AstRoot; import org.mozilla.javascript.ast.Block; import org.mozilla.javascript.ast.BreakStatement; +import org.mozilla.javascript.ast.CatchClause; import org.mozilla.javascript.ast.ConditionalExpression; import org.mozilla.javascript.ast.ContinueStatement; import org.mozilla.javascript.ast.DoLoop; @@ -43,7 +49,9 @@ import org.mozilla.javascript.ast.InfixExpression; import org.mozilla.javascript.ast.Label; import org.mozilla.javascript.ast.LabeledStatement; import org.mozilla.javascript.ast.LetNode; +import org.mozilla.javascript.ast.Name; import org.mozilla.javascript.ast.NewExpression; +import org.mozilla.javascript.ast.NodeVisitor; import org.mozilla.javascript.ast.NumberLiteral; import org.mozilla.javascript.ast.ObjectLiteral; import org.mozilla.javascript.ast.ObjectProperty; @@ -51,7 +59,16 @@ import org.mozilla.javascript.ast.ParenthesizedExpression; import org.mozilla.javascript.ast.PropertyGet; import org.mozilla.javascript.ast.RegExpLiteral; import org.mozilla.javascript.ast.ReturnStatement; +import org.mozilla.javascript.ast.Scope; import org.mozilla.javascript.ast.StringLiteral; +import org.mozilla.javascript.ast.SwitchCase; +import org.mozilla.javascript.ast.SwitchStatement; +import org.mozilla.javascript.ast.ThrowStatement; +import org.mozilla.javascript.ast.TryStatement; +import org.mozilla.javascript.ast.UnaryExpression; +import org.mozilla.javascript.ast.VariableDeclaration; +import org.mozilla.javascript.ast.VariableInitializer; +import org.mozilla.javascript.ast.WhileLoop; import org.teavm.codegen.SourceWriter; /** @@ -59,7 +76,6 @@ import org.teavm.codegen.SourceWriter; * @author Alexey Andreev */ public class AstWriter { - private static final int PRECEDENCE_PRIMARY = 1; private static final int PRECEDENCE_MEMBER = 2; private static final int PRECEDENCE_FUNCTION = 3; private static final int PRECEDENCE_POSTFIX = 4; @@ -78,17 +94,63 @@ public class AstWriter { private static final int PRECEDENCE_ASSIGN = 17; private static final int PRECEDENCE_COMMA = 18; private SourceWriter writer; + private Map nameMap = new HashMap<>(); + private Set aliases = new HashSet<>(); public AstWriter(SourceWriter writer) { this.writer = writer; } + private void declareName(String name) { + if (nameMap.containsKey(name)) { + return; + } + if (aliases.add(name)) { + nameMap.put(name, name); + return; + } + for (int i = 0;; ++i) { + String alias = name + "_" + i; + if (aliases.add(alias)) { + nameMap.put(name, alias); + return; + } + } + } + + public void declareAlias(String name, String alias) { + if (!aliases.add(alias)) { + throw new IllegalArgumentException("Alias " + alias + " is already occupied"); + } + nameMap.put(name, alias); + } + + public void hoist(AstNode node) { + node.visit(new NodeVisitor() { + @Override + public boolean visit(AstNode node) { + if (node instanceof Scope) { + Scope scope = (Scope) node; + if (scope.getSymbolTable() != null) { + for (String name : scope.getSymbolTable().keySet()) { + declareName(name); + } + } + } + return true; + } + }); + } + public void print(AstNode node) throws IOException { print(node, PRECEDENCE_COMMA); } private void print(AstNode node, int precedence) throws IOException { switch (node.getType()) { + case Token.SCRIPT: + print((AstRoot) node); + break; case Token.CALL: case Token.NEW: print((FunctionCall) node, precedence); @@ -111,6 +173,21 @@ public class AstWriter { case Token.STRING: print((StringLiteral) node); break; + case Token.TRUE: + writer.append("true"); + break; + case Token.FALSE: + writer.append("false"); + break; + case Token.THIS: + writer.append(nameMap.containsKey("this") ? nameMap.get("this") : "this"); + break; + case Token.NULL: + writer.append("null"); + break; + case Token.NAME: + print((Name) node); + break; case Token.REGEXP: print((RegExpLiteral) node); break; @@ -121,7 +198,11 @@ public class AstWriter { print((ArrayLiteral) node); break; case Token.BLOCK: - print((Block) node); + if (node instanceof Block) { + print((Block) node); + } else if (node instanceof Scope) { + print((Scope) node); + } break; case Token.HOOK: print((ConditionalExpression) node, precedence); @@ -170,14 +251,39 @@ public class AstWriter { case Token.IF: print((IfStatement) node); break; + case Token.SWITCH: + print((SwitchStatement) node); + break; + case Token.THROW: + print((ThrowStatement) node); + break; + case Token.TRY: + print((TryStatement) node); + break; + case Token.CONST: + case Token.VAR: + case Token.LET: + print((VariableDeclaration) node); + break; + case Token.WHILE: + print((WhileLoop) node); + break; default: if (node instanceof InfixExpression) { printInfix((InfixExpression) node, precedence); + } else if (node instanceof UnaryExpression) { + printUnary((UnaryExpression) node, precedence); } break; } } + private void print(AstRoot node) throws IOException { + for (Node child : node) { + print((AstNode) child); + } + } + private void print(Block node) throws IOException { writer.append('{').softNewLine().indent(); for (Node child = node.getFirstChild(); child != null; child = child.getNext()) { @@ -187,6 +293,15 @@ public class AstWriter { writer.outdent().append('}'); } + private void print(Scope node) throws IOException { + writer.append('{').softNewLine().indent(); + for (Node child = node.getFirstChild(); child != null; child = child.getNext()) { + print((AstNode) child); + writer.softNewLine(); + } + writer.outdent().append('}'); + } + private void print(LabeledStatement node) throws IOException { for (Label label : node.getLabels()) { writer.append(label.getName()).append(':').ws(); @@ -219,6 +334,12 @@ public class AstWriter { writer.append(';'); } + private void print(ThrowStatement node) throws IOException { + writer.append("throw "); + print(node.getExpression()); + writer.append(';'); + } + private void print(DoLoop node) throws IOException { writer.append("do"); if (node.getBody() instanceof Block) { @@ -256,6 +377,13 @@ public class AstWriter { print(node.getBody()); } + private void print(WhileLoop node) throws IOException { + writer.append("while").ws().append('('); + print(node.getCondition()); + writer.append(')').ws(); + print(node.getBody()); + } + private void print(IfStatement node) throws IOException { writer.append("if").ws().append('('); print(node.getCondition()); @@ -267,6 +395,78 @@ public class AstWriter { } } + private void print(SwitchStatement node) throws IOException { + writer.append("switch").ws().append('('); + print(node.getExpression()); + writer.append(')').ws().append('{').indent().softNewLine(); + for (SwitchCase sc : node.getCases()) { + if (sc.getExpression() == null) { + writer.append("default:"); + } else { + writer.append("case "); + print(sc.getExpression()); + writer.append(':'); + } + writer.indent().softNewLine(); + if (sc.getStatements() != null) { + for (AstNode stmt : sc.getStatements()) { + print(stmt); + writer.softNewLine(); + } + } + writer.outdent(); + } + writer.append('}'); + } + + private void print(TryStatement node) throws IOException { + writer.append("try "); + print(node.getTryBlock()); + for (CatchClause cc : node.getCatchClauses()) { + writer.ws().append("catch").ws().append('('); + print(cc.getVarName()); + if (cc.getCatchCondition() != null) { + writer.append(" if "); + print(cc.getCatchCondition()); + } + writer.append(')'); + } + if (node.getFinallyBlock() != null) { + writer.ws().append("finally "); + print(node.getFinallyBlock()); + } + } + + private void print(VariableDeclaration node) throws IOException { + switch (node.getType()) { + case Token.VAR: + writer.append("var "); + break; + case Token.LET: + writer.append("let "); + break; + case Token.CONST: + writer.append("const "); + break; + default: + break; + } + print(node.getVariables().get(0)); + for (int i = 1; i < node.getVariables().size(); ++i) { + writer.append(',').ws(); + print(node.getVariables().get(i)); + } + writer.append(';'); + } + + private void print(VariableInitializer node) throws IOException { + print(node.getTarget()); + if (node.getInitializer() != null) { + writer.ws().append('=').ws(); + print(node.getInitializer()); + } + } + private void print(ExpressionStatement node) throws IOException { print(node.getExpression()); writer.append(';'); @@ -389,6 +589,14 @@ public class AstWriter { writer.append(node.getQuoteCharacter()); } + private void print(Name node) throws IOException { + String alias = nameMap.get(node.getIdentifier()); + if (alias == null) { + alias = node.getIdentifier(); + } + writer.append(alias); + } + private void print(RegExpLiteral node) throws IOException { writer.append('/').append(node.getValue()).append('/').append(node.getFlags()); } @@ -428,8 +636,9 @@ public class AstWriter { if (!node.isMethod()) { writer.append("function"); } - if (node.getFunctionName() != null) { - writer.append(' ').append(node.getFunctionName()); + if (node.getFunctionName().getString() != null) { + writer.append(' '); + print(node.getFunctionName()); } writer.append('('); printList(node.getParams()); @@ -462,6 +671,31 @@ public class AstWriter { print(node.getExpression(), precedence); } + private void printUnary(UnaryExpression node, int precedence) throws IOException { + int innerPrecedence = node.isPostfix() ? PRECEDENCE_POSTFIX : PRECEDENCE_PREFIX; + + if (!node.isPostfix()) { + writer.append(AstNode.operatorToString(node.getType())); + if (requiresWhitespaces(node.getType())) { + writer.append(' '); + } + } + + if (innerPrecedence > precedence) { + writer.append('('); + } + + print(node.getOperand(), innerPrecedence); + + if (innerPrecedence > precedence) { + writer.append(')'); + } + + if (node.isPostfix()) { + writer.append(AstNode.operatorToString(node.getType())); + } + } + private void printInfix(InfixExpression node, int precedence) throws IOException { int innerPrecedence = getPrecedence(node.getType()); @@ -591,6 +825,8 @@ public class AstWriter { case Token.IN: case Token.TYPEOF: case Token.INSTANCEOF: + case Token.VOID: + case Token.DEL_REF: return true; default: return false; diff --git a/teavm-jso-impl/src/main/java/org/teavm/jso/plugin/JSParser.java b/teavm-jso-impl/src/main/java/org/teavm/jso/plugin/JSParser.java new file mode 100644 index 000000000..362cb9221 --- /dev/null +++ b/teavm-jso-impl/src/main/java/org/teavm/jso/plugin/JSParser.java @@ -0,0 +1,42 @@ +/* + * Copyright 2015 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.jso.plugin; + +import org.mozilla.javascript.CompilerEnvirons; +import org.mozilla.javascript.ErrorReporter; +import org.mozilla.javascript.Parser; + +/** + * + * @author Alexey Andreev + */ +public class JSParser extends Parser { + public JSParser(CompilerEnvirons compilerEnv, ErrorReporter errorReporter) { + super(compilerEnv, errorReporter); + } + + public JSParser(CompilerEnvirons compilerEnv) { + super(compilerEnv); + } + + public void enterFunction() { + ++nestingOfFunction; + } + + public void exitFunction() { + --nestingOfFunction; + } +} diff --git a/teavm-jso-impl/src/test/java/org/teavm/jso/plugin/AstWriterTest.java b/teavm-jso-impl/src/test/java/org/teavm/jso/plugin/AstWriterTest.java new file mode 100644 index 000000000..a67a57ab0 --- /dev/null +++ b/teavm-jso-impl/src/test/java/org/teavm/jso/plugin/AstWriterTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2015 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.jso.plugin; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import java.io.IOException; +import java.io.StringReader; +import org.junit.Test; +import org.mozilla.javascript.CompilerEnvirons; +import org.mozilla.javascript.ast.AstRoot; +import org.teavm.codegen.SourceWriter; +import org.teavm.codegen.SourceWriterBuilder; + +/** + * + * @author Alexey Andreev + */ +public class AstWriterTest { + private StringBuilder sb = new StringBuilder(); + private SourceWriter sourceWriter; + private AstWriter writer; + + public AstWriterTest() { + SourceWriterBuilder builder = new SourceWriterBuilder(null); + builder.setMinified(true); + sourceWriter = builder.build(sb); + writer = new AstWriter(sourceWriter); + } + + @Test + public void renamesVariable() throws IOException { + writer.declareAlias("a", "b"); + assertThat(transform("return a;"), is("return b;")); + } + + @Test + public void renamesDeclaredVariable() throws IOException { + writer.declareAlias("a", "b"); + assertThat(transform("var b; return a + b;"), is("var b_0;return b+b_0;")); + } + + @Test + public void writesReturn() throws IOException { + assertThat(transform("return x;"), is("return x;")); + } + + @Test + public void writesEmptyReturn() throws IOException { + assertThat(transform("return;"), is("return;")); + } + + @Test + public void writesThrow() throws IOException { + assertThat(transform("throw x;"), is("throw x;")); + } + + @Test + public void writesBreak() throws IOException { + assertThat(transform("a: while (true) { break a; }"), is("a:while(true){break a;}")); + } + + @Test + public void writesEmptyBreak() throws IOException { + assertThat(transform("while(true) { break; }"), is("while(true){break;}")); + } + + @Test + public void writesContinue() throws IOException { + assertThat(transform("a: while (true) { continue a; }"), is("a:while(true){continue a;}")); + } + + @Test + public void writesEmptyContinue() throws IOException { + assertThat(transform("while(true) { continue; }"), is("while(true){continue;}")); + } + + private String transform(String text) throws IOException { + CompilerEnvirons env = new CompilerEnvirons(); + env.setRecoverFromErrors(true); + JSParser factory = new JSParser(env); + factory.enterFunction(); + AstRoot rootNode = factory.parse(new StringReader(text), null, 0); + factory.exitFunction(); + writer.hoist(rootNode); + writer.print(rootNode); + return sb.toString(); + } +}