From 5b68b922b5542cb807c95635aaec67c38fda621a Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Mon, 24 Nov 2025 20:22:57 -0600 Subject: [PATCH 01/68] quick wip --- .../gwt/dev/jjs/JavaToJavaScriptCompiler.java | 3 + .../google/gwt/dev/jjs/ast/JIfStatement.java | 37 +++-- .../dev/jjs/impl/CatchBlockNormalizer.java | 2 +- .../google/gwt/dev/jjs/impl/ExpandBlocks.java | 153 ++++++++++++++++++ .../dev/jjs/impl/GenerateJavaScriptAST.java | 13 +- .../gwt/dev/jjs/impl/GwtAstBuilder.java | 6 +- .../jjs/impl/ImplementRecordComponents.java | 5 +- .../google/gwt/dev/jjs/impl/Simplifier.java | 12 +- .../jjs/impl/ToStringGenerationVisitor.java | 14 +- 9 files changed, 212 insertions(+), 33 deletions(-) create mode 100644 dev/core/src/com/google/gwt/dev/jjs/impl/ExpandBlocks.java diff --git a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java index efd7bf15a33..15283ffd776 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java +++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java @@ -76,6 +76,7 @@ import com.google.gwt.dev.jjs.impl.EnumNameObfuscator; import com.google.gwt.dev.jjs.impl.EnumOrdinalizer; import com.google.gwt.dev.jjs.impl.EqualityNormalizer; +import com.google.gwt.dev.jjs.impl.ExpandBlocks; import com.google.gwt.dev.jjs.impl.Finalizer; import com.google.gwt.dev.jjs.impl.FixAssignmentsToUnboxOrCast; import com.google.gwt.dev.jjs.impl.FullOptimizerContext; @@ -368,6 +369,8 @@ private PermutationResult compilePermutation(Permutation permutation, UnifiedAst // TODO(rluble): eventually move to normizeSemantics. CompileTimeConstantsReplacer.exec(jprogram); + ExpandBlocks.exec(jprogram); + // TODO(stalcup): move to after normalize. // (3) Optimize the resolved Java AST optimizeJava(); diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/JIfStatement.java b/dev/core/src/com/google/gwt/dev/jjs/ast/JIfStatement.java index 53eb6fec998..f4f0d9fa972 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/ast/JIfStatement.java +++ b/dev/core/src/com/google/gwt/dev/jjs/ast/JIfStatement.java @@ -22,18 +22,18 @@ */ public class JIfStatement extends JStatement { - private JStatement elseStmt; + private JBlock elseStmt; private JExpression ifExpr; - private JStatement thenStmt; + private JBlock thenStmt; - public JIfStatement(SourceInfo info, JExpression ifExpr, JStatement thenStmt, JStatement elseStmt) { + public JIfStatement(SourceInfo info, JExpression ifExpr, JBlock thenStmt, JBlock elseStmt) { super(info); this.ifExpr = ifExpr; - this.thenStmt = thenStmt; - this.elseStmt = elseStmt; + this.thenStmt = thenStmt == null ? new JBlock(info) : thenStmt; + this.elseStmt = elseStmt == null ? new JBlock(info) : elseStmt; } - public JStatement getElseStmt() { + public JBlock getElseStmt() { return elseStmt; } @@ -41,7 +41,7 @@ public JExpression getIfExpr() { return ifExpr; } - public JStatement getThenStmt() { + public JBlock getThenStmt() { return thenStmt; } @@ -49,16 +49,27 @@ public JStatement getThenStmt() { public void traverse(JVisitor visitor, Context ctx) { if (visitor.visit(this, ctx)) { ifExpr = visitor.accept(ifExpr); - if (thenStmt != null) { - thenStmt = visitor.accept(thenStmt, true); - } - if (elseStmt != null) { - elseStmt = visitor.accept(elseStmt, true); - } +// if (thenStmt != null) { + thenStmt = ensureBlock(visitor.accept(thenStmt, true)); +// } +// if (elseStmt != null) { + elseStmt = ensureBlock(visitor.accept(elseStmt, true)); +// } } visitor.endVisit(this, ctx); } + private JBlock ensureBlock(JStatement statement) { + if (statement == null) { + return new JBlock(getSourceInfo()); + } + if (statement instanceof JBlock) { + return (JBlock) statement; + } + + return new JBlock(statement.getSourceInfo(), statement); + } + @Override public boolean unconditionalControlBreak() { boolean thenBreaks = thenStmt != null && thenStmt.unconditionalControlBreak(); diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/CatchBlockNormalizer.java b/dev/core/src/com/google/gwt/dev/jjs/impl/CatchBlockNormalizer.java index c0c067c7993..1083f0bb580 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/impl/CatchBlockNormalizer.java +++ b/dev/core/src/com/google/gwt/dev/jjs/impl/CatchBlockNormalizer.java @@ -119,7 +119,7 @@ public void endVisit(JTryStatement x, Context ctx) { new JDeclarationStatement(catchInfo, arg, exceptionVariable.makeRef(catchInfo)); block.addStmt(0, declaration); // nest the previous as an else for me - cur = new JIfStatement(catchInfo, ifTest, block, cur); + cur = new JIfStatement(catchInfo, ifTest, block, new JBlock(cur.getSourceInfo(), cur)); } newCatchBlock.addStmt(cur); diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/ExpandBlocks.java b/dev/core/src/com/google/gwt/dev/jjs/impl/ExpandBlocks.java new file mode 100644 index 00000000000..d1034ad87dd --- /dev/null +++ b/dev/core/src/com/google/gwt/dev/jjs/impl/ExpandBlocks.java @@ -0,0 +1,153 @@ +package com.google.gwt.dev.jjs.impl; + +import com.google.gwt.dev.jjs.ast.Context; +import com.google.gwt.dev.jjs.ast.JBlock; +import com.google.gwt.dev.jjs.ast.JDoStatement; +import com.google.gwt.dev.jjs.ast.JForStatement; +import com.google.gwt.dev.jjs.ast.JIfStatement; +import com.google.gwt.dev.jjs.ast.JMethod; +import com.google.gwt.dev.jjs.ast.JMethodBody; +import com.google.gwt.dev.jjs.ast.JModVisitor; +import com.google.gwt.dev.jjs.ast.JProgram; +import com.google.gwt.dev.jjs.ast.JStatement; +import com.google.gwt.dev.jjs.ast.JSwitchStatement; +import com.google.gwt.dev.jjs.ast.JTryStatement; +import com.google.gwt.dev.jjs.ast.JWhileStatement; +import com.google.gwt.dev.js.ast.JsBlock; + +import java.util.Set; +import java.util.Stack; + +/** + * Examines blocks where child blocks (if/else, try/catch) use control flow statements + * (return/throw/break/continue) which make it impossible for that block to flow back to the parent. + * In these cases, the other block can be expanded to include the rest of the parent block. + * + * Examples: + * if (condition) { statements; return; } rest of parent block; + * or + * if (condition) { statements; return; } else { more; } rest of parent block; + * -> if (condition) { statements; } else { [more;] rest of parent block; } + * + * try { statements; return; } catch (e) { handler(e); } rest of parent block; + * -> try { statements; } catch (e) { handler(e); rest of parent block; } + * + * These transformations have two simple benefits today: + * + * + * For cases where an else block is added, we will also add a JS pass to remove it, transforming + * if (condition) { ... return; } else { ... } -> if (condition) { ... return; } ... + * so that there is no net increase in size. This will could also save a few bytes when the else + * already existed, and can now be removed. + * + * Only needs to be run once as part of normalization, since MethodInliner will never move more than + * an expression (either in a JExpressionStatement or JReturnStatement). + * + * + * if (a) { return; } if (b) {return;} rest; + * --> if (a) { return; } else { if (b) { return; } else { rest; } } + * + * if (a) { for (..) { if (b) { break; } rest1; } } rest2; + * --> if (a) { for (..) { if (b) { break; } else { rest1; } } } else { rest2; } + * + */ +public class ExpandBlocks { + /** + * Normalize the program's nested blocks. + */ + public static void exec(JProgram program) { + new JModVisitor() { + + private JBlock acceptor; + @Override + public void endVisit(JIfStatement x, Context ctx) { + if (x.getThenStmt().unconditionalControlBreak()) { + if (x.getElseStmt().unconditionalControlBreak()) { + // Both branches break, nothing to do - parent block is unreachable code after if/else + acceptor = null; + } else { + // else can handle rest of parent block + acceptor = x.getElseStmt(); + } + } else { + if (x.getThenStmt().unconditionalControlBreak()) { + // else can handle rest of parent block + acceptor = x.getElseStmt(); + } else { + // neither branch breaks, cannot optimize + acceptor = null; + } + } + } + + @Override + public void endVisit(JTryStatement x, Context ctx) { + if (x.getFinallyBlock() != null) { + // Cannot optimize if there is a finally block, as finally executes after catch and before remainder + acceptor = null; + return; + } + + if (!x.getTryBlock().unconditionalControlBreak()) { + // Try must unconditionally break to optimize + acceptor = null; + return; + } + + if (x.getCatchClauses().size() != 1) { + // Only can optimize exactly one catch, and unconditional return try + acceptor = null; + return; + } + acceptor = x.getCatchClauses().get(0).getBlock(); + } + + @Override + public void endVisit(JSwitchStatement x, Context ctx) { + //TODO handle switch case (though probably rarely worth it) + acceptor = null; + } + + @Override + public void endVisit(JForStatement x, Context ctx) { + // Interrupts moving statements to the last acceptor block + acceptor = null; + } + + @Override + public void endVisit(JWhileStatement x, Context ctx) { + // Interrupts moving statements to the last acceptor block + acceptor = null; + } + + @Override + public void endVisit(JDoStatement x, Context ctx) { + // Interrupts moving statements to the last acceptor block + acceptor = null; + } + + + @Override + public boolean visit(JStatement x, Context ctx) { + if (acceptor != null) { + // If there is a block ready to accept this, any statement should be moved. + // Any visit() override must call super before it does its own work, + // to ensure that it is relocated + ctx.removeMe(); + acceptor.addStmt(x); + // Moved the item itself, continue to see if it needs to adopt later statements + } + return super.visit(x, ctx); + } + + @Override + public void endVisit(JMethodBody x, Context ctx) { + // Clear any acceptor at end of method body + acceptor = null; + } + }.accept(program); + } +} diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java index 38941fb7e07..f7ec450a44f 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java +++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java @@ -574,7 +574,7 @@ public JsExpression transformBinaryOperation(JBinaryOperation binaryOperation) { } @Override - public JsStatement transformBlock(JBlock block) { + public JsBlock transformBlock(JBlock block) { JsBlock jsBlock = new JsBlock(block.getSourceInfo()); List stmts = jsBlock.getStatements(); @@ -753,12 +753,19 @@ public JsNode transformIfStatement(JIfStatement ifStatement) { result.setIfExpr(transform(ifStatement.getIfExpr())); result.setThenStmt(jsEmptyIfNull(ifStatement.getSourceInfo(), - transform(ifStatement.getThenStmt()))); - result.setElseStmt(transform(ifStatement.getElseStmt())); + unwrapSingleStatement(transformBlock(ifStatement.getThenStmt())))); + result.setElseStmt(unwrapSingleStatement(transformBlock(ifStatement.getElseStmt()))); return result; } + private JsStatement unwrapSingleStatement(JsBlock block) { + if (block.getStatements().size() == 1) { + return block.getStatements().get(0); + } + return block; + } + @Override public JsLabel transformLabel(JLabel label) { return new JsLabel(label.getSourceInfo(), names.get(label)); diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java index eb8b806cc27..3505c524e5f 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java +++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java @@ -1093,8 +1093,8 @@ public JExpression apply(JStatement statement) { public void endVisit(IfStatement x, BlockScope scope) { try { SourceInfo info = makeSourceInfo(x); - JStatement elseStatement = pop(x.elseStatement); - JStatement thenStatement = pop(x.thenStatement); + JBlock elseStatement = popBlock(info, x.elseStatement); + JBlock thenStatement = popBlock(info, x.thenStatement); JExpression condition = pop(x.condition); push(new JIfStatement(info, condition, thenStatement, elseStatement)); } catch (Throwable e) { @@ -2454,7 +2454,7 @@ private JBlock normalizeTryWithResources(SourceInfo info, TryStatement x, JBlock JExpression exceptionNotNull = new JBinaryOperation(info, JPrimitiveType.BOOLEAN, JBinaryOperator.NEQ, exceptionVar.makeRef(info), JNullLiteral.INSTANCE); finallyBlock.addStmt(new JIfStatement(info, exceptionNotNull, - new JThrowStatement(info, exceptionVar.makeRef(info)), null)); + new JBlock(info, new JThrowStatement(info, exceptionVar.makeRef(info))), null)); // Stitch all together into a inner try block outerTryBlock.addStmt(new JTryStatement(info, tryBlock, catchClauses, diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/ImplementRecordComponents.java b/dev/core/src/com/google/gwt/dev/jjs/impl/ImplementRecordComponents.java index 19c9335d74d..c7f4e8ec978 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/impl/ImplementRecordComponents.java +++ b/dev/core/src/com/google/gwt/dev/jjs/impl/ImplementRecordComponents.java @@ -18,6 +18,7 @@ import com.google.gwt.dev.jjs.SourceInfo; import com.google.gwt.dev.jjs.ast.JBinaryOperation; import com.google.gwt.dev.jjs.ast.JBinaryOperator; +import com.google.gwt.dev.jjs.ast.JBlock; import com.google.gwt.dev.jjs.ast.JBooleanLiteral; import com.google.gwt.dev.jjs.ast.JClassLiteral; import com.google.gwt.dev.jjs.ast.JClassType; @@ -160,7 +161,7 @@ private void implementEquals(JRecordType type, JMethod method, SourceInfo info) new JThisRef(info, type), otherParam.createRef(info)); body.getBlock().addStmt(new JIfStatement(info, eq, - JBooleanLiteral.TRUE.makeReturnStatement(), null)); + new JBlock(info, JBooleanLiteral.TRUE.makeReturnStatement()), null)); // other == null JBinaryOperation nonNullCheck = @@ -178,7 +179,7 @@ private void implementEquals(JRecordType type, JMethod method, SourceInfo info) // if (other == null || MyRecordType.class != other.getClass()) return false; body.getBlock().addStmt(new JIfStatement(info, nullAndTypeCheck, - JBooleanLiteral.FALSE.makeReturnStatement(), null)); + new JBlock(info, JBooleanLiteral.FALSE.makeReturnStatement()), null)); // Create a local to assign to and compare each component JLocal typedOther = JProgram.createLocal(info, "other", type, true, body); diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/Simplifier.java b/dev/core/src/com/google/gwt/dev/jjs/impl/Simplifier.java index d7dd069accc..fa59a85e435 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/impl/Simplifier.java +++ b/dev/core/src/com/google/gwt/dev/jjs/impl/Simplifier.java @@ -240,8 +240,8 @@ public static JExpression simplifyConditional(JConditional expression) { public static JStatement simplifyIfStatement(JIfStatement ifStatement, JType methodReturnType) { SourceInfo info = ifStatement.getSourceInfo(); JExpression conditionExpression = ifStatement.getIfExpr(); - JStatement thenStmt = ifStatement.getThenStmt(); - JStatement elseStmt = ifStatement.getElseStmt(); + JBlock thenStmt = ifStatement.getThenStmt(); + JBlock elseStmt = ifStatement.getElseStmt(); if (conditionExpression instanceof JMultiExpression) { // if(a,b,c) d else e -> {a; b; if(c) d else e; } JMultiExpression condMulti = (JMultiExpression) conditionExpression; @@ -279,10 +279,14 @@ public static JStatement simplifyIfStatement(JIfStatement ifStatement, JType met JExpression negationArugment = Simplifier.maybeGetNegatedExpressionArgument(conditionExpression); if (negationArugment != null) { + + // Force sub-parts to blocks, otherwise we break else-if chains. // TODO: this goes away when we normalize the Java AST properly. - thenStmt = ensureBlock(thenStmt); - elseStmt = ensureBlock(elseStmt); + + +// thenStmt = ensureBlock(thenStmt); +// elseStmt = ensureBlock(elseStmt); return simplifyIfStatement( new JIfStatement(info, negationArugment, elseStmt, thenStmt), methodReturnType); } diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/ToStringGenerationVisitor.java b/dev/core/src/com/google/gwt/dev/jjs/impl/ToStringGenerationVisitor.java index f42eded388c..594090e4344 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/impl/ToStringGenerationVisitor.java +++ b/dev/core/src/com/google/gwt/dev/jjs/impl/ToStringGenerationVisitor.java @@ -552,16 +552,16 @@ public boolean visit(JIfStatement x, Context ctx) { needSemi = true; } print(CHARS_ELSE); - boolean elseIf = x.getElseStmt() instanceof JIfStatement; - if (!elseIf) { +// boolean elseIf = x.getElseStmt() instanceof JIfStatement; +// if (!elseIf) { nestedStatementPush(x.getElseStmt()); - } else { - space(); - } +// } else { +// space(); +// } accept(x.getElseStmt()); - if (!elseIf) { +// if (!elseIf) { nestedStatementPop(x.getElseStmt()); - } +// } } return false; From 262b13ba70140e39a27dfe7cbed31496546ae086 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Tue, 25 Nov 2025 20:34:40 -0600 Subject: [PATCH 02/68] Vaguely functional implementation, except for switches --- .../google/gwt/dev/jjs/impl/ExpandBlocks.java | 179 ++++++++++++++---- 1 file changed, 146 insertions(+), 33 deletions(-) diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/ExpandBlocks.java b/dev/core/src/com/google/gwt/dev/jjs/impl/ExpandBlocks.java index d1034ad87dd..4eaafa62aec 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/impl/ExpandBlocks.java +++ b/dev/core/src/com/google/gwt/dev/jjs/impl/ExpandBlocks.java @@ -1,3 +1,18 @@ +/* + * Copyright 2025 GWT Project Authors + * + * 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 com.google.gwt.dev.jjs.impl; import com.google.gwt.dev.jjs.ast.Context; @@ -5,7 +20,6 @@ import com.google.gwt.dev.jjs.ast.JDoStatement; import com.google.gwt.dev.jjs.ast.JForStatement; import com.google.gwt.dev.jjs.ast.JIfStatement; -import com.google.gwt.dev.jjs.ast.JMethod; import com.google.gwt.dev.jjs.ast.JMethodBody; import com.google.gwt.dev.jjs.ast.JModVisitor; import com.google.gwt.dev.jjs.ast.JProgram; @@ -13,25 +27,23 @@ import com.google.gwt.dev.jjs.ast.JSwitchStatement; import com.google.gwt.dev.jjs.ast.JTryStatement; import com.google.gwt.dev.jjs.ast.JWhileStatement; -import com.google.gwt.dev.js.ast.JsBlock; -import java.util.Set; import java.util.Stack; /** * Examines blocks where child blocks (if/else, try/catch) use control flow statements * (return/throw/break/continue) which make it impossible for that block to flow back to the parent. * In these cases, the other block can be expanded to include the rest of the parent block. - * + *

* Examples: * if (condition) { statements; return; } rest of parent block; * or * if (condition) { statements; return; } else { more; } rest of parent block; * -> if (condition) { statements; } else { [more;] rest of parent block; } - * + *

* try { statements; return; } catch (e) { handler(e); } rest of parent block; * -> try { statements; } catch (e) { handler(e); rest of parent block; } - * + *

* These transformations have two simple benefits today: *