Skip to content

Commit a2cf6c5

Browse files
committed
Improve condensing pattern expressions
1 parent 2eb7b89 commit a2cf6c5

4 files changed

Lines changed: 243 additions & 84 deletions

File tree

src/org/benf/cfr/reader/bytecode/analysis/opgraph/op3rewriters/CondenseConditionals.java

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -457,14 +457,31 @@ private static boolean condenseConditional2_type1(Op03SimpleStatement ifStatemen
457457
*
458458
* if (!(o instanceof T)) goto ELSE
459459
* T b = (T)o;
460-
* if (!cond) goto ELSE
460+
* <continuation>
461461
*
462-
* Absorb the cast+assign into the first condition (without reordering):
462+
* where <continuation> may be either another `if (!cond) goto ELSE` (chain
463+
* shape) or ordinary body code (bare / chain-tail shape).
464+
*
465+
* In either case, absorb the cast+assign into the first condition (without
466+
* reordering) using the synthetic null-check trick:
463467
*
464468
* if (!(o instanceof T && null != (b = (T)o))) goto ELSE
465-
* if (!cond) goto ELSE
469+
* <continuation>
470+
*
471+
* `null != (b = (T)o)` is tautologically true under the instanceof guard
472+
* (instanceof excludes null; cast of non-null T is non-null), so the
473+
* boolean semantics are unchanged; the assignment lives as a side effect
474+
* inside the condition.
466475
*
467-
* Now the two ifs are adjacent and condenseConditionals can merge them.
476+
* For the chain shape, S0 and S2 are now adjacent same-target ifs and
477+
* condenseConditionals merges them. For the bare/tail shape there is
478+
* nothing to merge — S2 is just body. Either way, scope discovery's
479+
* j16 lift (InstanceOfAssignRewriter) then either turns the absorbed
480+
* condition into instanceof T b && null != b (when b is a fresh local),
481+
* or leaves it as instanceof T && null != (b = (T)o) (when b is
482+
* method-scoped). InstanceOfMatchCheckTransformer is the final pass that
483+
* either elides the redundant null!=b clause or reverse-lifts the
484+
* still-absorbed assignment back into a body statement.
468485
*/
469486
public static boolean condenseInstanceOfAssign(List<Op03SimpleStatement> statements) {
470487
boolean effect = false;
@@ -495,13 +512,27 @@ public static boolean condenseInstanceOfAssign(List<Op03SimpleStatement> stateme
495512
if (!instanceOf.getLhs().equals(cast.getChild())) continue;
496513

497514
Op03SimpleStatement s2 = s1.getTargets().get(0);
498-
Statement s2inner = s2.getStatement();
499-
if (!(s2inner instanceof IfStatement)) continue;
500515
if (s2.getSources().size() != 1) continue;
501516

502-
Op03SimpleStatement s2taken = s2.getTargets().get(1);
503-
// Both ifs must jump to the same ELSE target
504-
if (s0taken != s2taken) continue;
517+
// If S2 is another `if`, require it shares S0's taken (ELSE) target so
518+
// condenseConditionals can later merge them. If S2 is anything else
519+
// (the bare/tail case), just absorb — nothing to merge with.
520+
//
521+
// Refuse if S2 is an unconditional jump (`goto`): that indicates the
522+
// cast-assign sits between the if-taken edge and a branch-rejoin
523+
// (typically the negated-pattern shape `if (!(o instanceof T s))
524+
// {...} else {use(s)}`, where `b = (T)o; goto THEN` skips over the
525+
// else-block). Absorbing into the condition there destroys the
526+
// structure-recovery hint that this is an if/else.
527+
Statement s2inner = s2.getStatement();
528+
// Note: IfStatement extends GotoStatement, so use exact-class equality
529+
// for the goto refusal — `instanceof GotoStatement` would also match
530+
// IfStatement and wrongly suppress the chain-merge case.
531+
if (s2inner.getClass() == GotoStatement.class) continue;
532+
if (s2inner instanceof IfStatement) {
533+
Op03SimpleStatement s2taken = s2.getTargets().get(1);
534+
if (s0taken != s2taken) continue;
535+
}
505536

506537
// Absorb: create null != (b = (T)o) as a ConditionalExpression
507538
Expression assignExpr = assign.getInliningExpression();
Lines changed: 191 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,76 @@
11
package org.benf.cfr.reader.bytecode.analysis.opgraph.op4rewriters.transformers;
22

3+
import org.benf.cfr.reader.bytecode.analysis.loc.BytecodeLoc;
34
import org.benf.cfr.reader.bytecode.analysis.opgraph.Op04StructuredStatement;
4-
import org.benf.cfr.reader.bytecode.analysis.opgraph.op4rewriters.ExpressionReplacingRewriter;
55
import org.benf.cfr.reader.bytecode.analysis.parse.Expression;
66
import org.benf.cfr.reader.bytecode.analysis.parse.LValue;
7+
import org.benf.cfr.reader.bytecode.analysis.parse.expression.AssignmentExpression;
78
import org.benf.cfr.reader.bytecode.analysis.parse.expression.BoolOp;
89
import org.benf.cfr.reader.bytecode.analysis.parse.expression.BooleanExpression;
910
import org.benf.cfr.reader.bytecode.analysis.parse.expression.BooleanOperation;
1011
import org.benf.cfr.reader.bytecode.analysis.parse.expression.CastExpression;
12+
import org.benf.cfr.reader.bytecode.analysis.parse.expression.CompOp;
13+
import org.benf.cfr.reader.bytecode.analysis.parse.expression.ComparisonOperation;
1114
import org.benf.cfr.reader.bytecode.analysis.parse.expression.ConditionalExpression;
1215
import org.benf.cfr.reader.bytecode.analysis.parse.expression.InstanceOfExpression;
1316
import org.benf.cfr.reader.bytecode.analysis.parse.expression.InstanceOfExpressionDefining;
17+
import org.benf.cfr.reader.bytecode.analysis.parse.expression.Literal;
18+
import org.benf.cfr.reader.bytecode.analysis.parse.expression.LValueExpression;
19+
import org.benf.cfr.reader.bytecode.analysis.types.JavaTypeInstance;
1420
import org.benf.cfr.reader.bytecode.analysis.structured.StructuredScope;
1521
import org.benf.cfr.reader.bytecode.analysis.structured.StructuredStatement;
1622
import org.benf.cfr.reader.bytecode.analysis.structured.statement.Block;
1723
import org.benf.cfr.reader.bytecode.analysis.structured.statement.StructuredAssignment;
1824
import org.benf.cfr.reader.bytecode.analysis.structured.statement.StructuredIf;
19-
import org.benf.cfr.reader.bytecode.analysis.types.JavaTypeInstance;
25+
import org.benf.cfr.reader.util.collections.ListFactory;
26+
import org.benf.cfr.reader.util.collections.SetFactory;
2027

28+
import java.util.Iterator;
29+
import java.util.LinkedList;
2130
import java.util.List;
31+
import java.util.Set;
2232

2333
/*
24-
* Lift the j18+ post-pattern-matching shape:
25-
*
26-
* if (obj instanceof T) {
27-
* T s = (T) obj;
28-
* ...
29-
* }
34+
* Cleanup pass for the j18+ instanceof pattern shape that op03's
35+
* condenseInstanceOfAssign and scope discovery's j16 lift produce together.
3036
*
31-
* into the pattern form:
37+
* We absorb every cast-assign into its preceding if's condition with the
38+
* synthetic null-check trick:
3239
*
33-
* if (obj instanceof T s) {
34-
* ...
35-
* }
40+
* if (!(o instanceof T)) goto E; \
41+
* T b = (T)o; > ==> if (!(o instanceof T && null != (b = (T)o))) goto E;
42+
* <continuation> / <continuation>
3643
*
37-
* The j14/j16 contrived shape (cast+assign folded inside the condition via a
38-
* self-comparison) is handled by InstanceOfAssignRewriter at scope-discovery
39-
* time. j18+ emits the straightforward shape above instead, so the lift has
40-
* to happen post-hoc here.
44+
* Then scope discovery, via InstanceOfAssignRewriter's MatchType.SIMPLE_J16,
45+
* tries to lift the bound variable into the instanceof:
46+
* - SUCCESS (b is a fresh local): condition becomes
47+
* instanceof T b && null != b
48+
* The null-check is now redundant — instanceof T b implies b != null —
49+
* but no earlier pass elides it.
50+
* - REFUSAL (b is method-scoped, used outside the if): condition stays
51+
* instanceof T && null != (b = (T)o)
52+
* The original pre-pattern code was nicer than this absorbed form.
4153
*
42-
* Also completes the compound case set up by the op03 condense pass that
43-
* turns `instanceof T s && cond` j21 bytecode into:
54+
* This pass walks each StructuredIf's top-level AND chain and:
4455
*
45-
* if (obj instanceof T && cond) {
46-
* s = (T) obj;
47-
* ...
48-
* }
56+
* (1) Drops `null != b` operands (in that exact order — the canary shape
57+
* this pipeline synthesises) when a sibling defines b via
58+
* `instanceof T b`.
59+
* (2) Pushes a *trailing* `null != (b = (T)o)` operand down into the body
60+
* as an assignment statement, when no sibling defines b. Only the LAST
61+
* operand qualifies — see correctness note below.
4962
*
50-
* by collapsing the leading assignment into an instanceof-defining pattern.
63+
* Correctness note on the push-down:
64+
* Original: instanceof T && null != (b = (T)o) && cond2
65+
* - instanceof T true, cond2 false → the && evaluates left-to-right, the
66+
* assignment side-effect of the middle clause runs BEFORE cond2 is
67+
* checked, so b IS assigned even when the if isn't taken.
68+
* Naive push-down to: if (instanceof T && cond2) { b = (T)o; ... }
69+
* - cond2 false → body not entered → b NOT assigned. Differs.
70+
* For a method-scoped b that's read after the if, this is observable.
71+
* Therefore push the assignment down only if it is the LAST operand, where
72+
* no later condition can short-circuit and the side effect would have been
73+
* observable only on the if-taken path anyway.
5174
*/
5275
public class InstanceOfMatchCheckTransformer implements StructuredStatementTransformer {
5376

@@ -60,77 +83,170 @@ public void transform(Op04StructuredStatement root) {
6083
public StructuredStatement transform(StructuredStatement in, StructuredScope scope) {
6184
in.transformStructuredChildren(this, scope);
6285
if (in instanceof StructuredIf) {
63-
tryLift((StructuredIf) in);
86+
tidy((StructuredIf) in);
6487
}
6588
return in;
6689
}
6790

68-
private static void tryLift(StructuredIf sif) {
69-
Op04StructuredStatement firstContainer = firstStatementOf(sif.getIfTaken());
70-
if (firstContainer == null) return;
71-
StructuredStatement first = firstContainer.getStatement();
72-
if (!(first instanceof StructuredAssignment)) return;
73-
StructuredAssignment assign = (StructuredAssignment) first;
91+
private static void tidy(StructuredIf sif) {
92+
ConditionalExpression cond = sif.getConditionalExpression();
93+
List<ConditionalExpression> operands = ListFactory.newList();
94+
flattenAnd(cond, operands);
95+
if (operands.size() <= 1) return;
7496

75-
// Lift only when the assignment IS the variable's creation point.
76-
LValue lvalue = assign.getLvalue();
77-
if (!assign.isCreator(lvalue)) return;
97+
Set<LValue> defined = SetFactory.newSet();
98+
for (ConditionalExpression op : operands) {
99+
LValue d = getDefinedLValue(op);
100+
if (d != null) defined.add(d);
101+
}
78102

79-
if (!(assign.getRvalue() instanceof CastExpression)) return;
80-
CastExpression cast = (CastExpression) assign.getRvalue();
81-
Expression castSubject = cast.getChild();
82-
JavaTypeInstance castType = cast.getInferredJavaType().getJavaTypeInstance();
103+
boolean changed = false;
104+
AssignmentExpression pushDown = null;
83105

84-
InstanceOfExpression target = findReachable(sif.getConditionalExpression(), castSubject, castType);
85-
if (target == null) return;
106+
// (2) Push-down candidate: only the LAST operand, only if it's our
107+
// canary's full shape — `null != (b = (T)o)` paired with a plain
108+
// `instanceof T` sibling whose subject and type match the cast.
109+
// The sibling-match is what makes the rewrite semantics-preserving:
110+
// with `instanceof T` true on the if-taken path, `(T)o` is provably
111+
// non-null, so the null-check it carries is dead weight. Without
112+
// that pairing (e.g. a user-written `null != (b = somefunc())`), the
113+
// null-check is doing real work and we must NOT push it down — see
114+
// InstanceOfPatternTest20 for the negative case.
115+
ConditionalExpression last = operands.get(operands.size() - 1);
116+
AssignmentExpression absorbed = getAbsorbedAssignment(last);
117+
if (absorbed != null
118+
&& !defined.contains(absorbed.getlValue())
119+
&& hasMatchingInstanceofSibling(absorbed, operands)) {
120+
pushDown = absorbed;
121+
operands.remove(operands.size() - 1);
122+
changed = true;
123+
}
86124

87-
InstanceOfExpressionDefining replacement = new InstanceOfExpressionDefining(
88-
target.getLoc(),
89-
target.getInferredJavaType(),
90-
target.getLhs(),
91-
castType,
92-
lvalue
93-
);
125+
// (1) Drop redundant null-checks of variables bound by a sibling defining instanceof.
126+
Iterator<ConditionalExpression> it = operands.iterator();
127+
while (it.hasNext()) {
128+
LValue v = getNullCheckedLValue(it.next());
129+
if (v != null && defined.contains(v)) {
130+
it.remove();
131+
changed = true;
132+
}
133+
}
94134

95-
sif.rewriteExpressions(new ExpressionReplacingRewriter(target, replacement));
96-
firstContainer.nopOut();
97-
}
135+
if (!changed) return;
136+
if (operands.isEmpty()) return; // defensive — shouldn't happen with well-formed input
98137

99-
private static Op04StructuredStatement firstStatementOf(Op04StructuredStatement s) {
100-
StructuredStatement stmt = s.getStatement();
101-
if (stmt instanceof Block) {
102-
List<Op04StructuredStatement> bs = ((Block) stmt).getBlockStatements();
103-
if (bs.isEmpty()) return null;
104-
return bs.get(0);
138+
ConditionalExpression rebuilt = operands.get(0);
139+
for (int i = 1; i < operands.size(); i++) {
140+
rebuilt = new BooleanOperation(BytecodeLoc.NONE, rebuilt, operands.get(i), BoolOp.AND);
141+
}
142+
sif.setConditionalExpression(rebuilt);
143+
144+
if (pushDown != null) {
145+
prependAssignment(sif, pushDown);
105146
}
106-
return s;
107147
}
108148

109149
/*
110-
* Find an InstanceOfExpression matching (subject, type) reachable from the
111-
* top of `cond` purely via &&-chain (and BooleanExpression wrapping). An
112-
* instanceof to the rhs of a disjunction, or inside a not doesn't positively assign
113-
* the then branch (though a 'not' can assign the else branch (!))
150+
* Add the assignment to the start of the block inside the conditional.
114151
*/
115-
private static InstanceOfExpression findReachable(Expression cond, Expression subject, JavaTypeInstance type) {
116-
if (cond instanceof BooleanExpression) {
117-
return findReachable(((BooleanExpression) cond).getInner(), subject, type);
152+
private static void prependAssignment(StructuredIf sif, AssignmentExpression assign) {
153+
StructuredAssignment newAssign = new StructuredAssignment(
154+
BytecodeLoc.NONE, assign.getlValue(), assign.getrValue(), false);
155+
Op04StructuredStatement newAssignContainer = new Op04StructuredStatement(newAssign);
156+
157+
Op04StructuredStatement ifTaken = sif.getIfTaken();
158+
StructuredStatement body = ifTaken.getStatement();
159+
if (body instanceof Block) {
160+
((Block) body).getBlockStatements().add(0, newAssignContainer);
161+
} else {
162+
LinkedList<Op04StructuredStatement> kids = new LinkedList<Op04StructuredStatement>();
163+
kids.add(newAssignContainer);
164+
kids.add(new Op04StructuredStatement(body));
165+
ifTaken.replaceStatement(new Block(kids, true));
118166
}
119-
if (cond instanceof InstanceOfExpression) {
120-
InstanceOfExpression ioe = (InstanceOfExpression) cond;
121-
if (ioe.getLhs().equals(subject) && ioe.getTypeInstance().equals(type)) {
122-
return ioe;
123-
}
124-
return null;
167+
}
168+
169+
// Todo : This feels like it should be utility elsewhere.
170+
private static void flattenAnd(ConditionalExpression e, List<ConditionalExpression> out) {
171+
if (e instanceof BooleanOperation && ((BooleanOperation) e).getOp() == BoolOp.AND) {
172+
BooleanOperation bo = (BooleanOperation) e;
173+
flattenAnd(bo.getLhs(), out);
174+
flattenAnd(bo.getRhs(), out);
175+
return;
125176
}
126-
if (cond instanceof BooleanOperation) {
127-
BooleanOperation bo = (BooleanOperation) cond;
128-
if (bo.getOp() != BoolOp.AND) return null;
129-
InstanceOfExpression r = findReachable(bo.getLhs(), subject, type);
130-
if (r != null) return r;
131-
return findReachable(bo.getRhs(), subject, type);
177+
out.add(e);
178+
}
179+
180+
/*
181+
* InstanceOfExpressionDefining extends AbstractExpression but does NOT
182+
* implement ConditionalExpression — it must be wrapped in BooleanExpression
183+
* to appear inside a boolean condition. Since `op` is typed as a
184+
* ConditionalExpression, the only way to see an InstanceOfExpressionDefining
185+
* here is through that wrap. ComparisonOperation by contrast already
186+
* implements ConditionalExpression directly, so the matchers below don't
187+
* need to unwrap.
188+
*/
189+
private static LValue getDefinedLValue(ConditionalExpression op) {
190+
if (!(op instanceof BooleanExpression)) return null;
191+
Expression inner = ((BooleanExpression) op).getInner();
192+
if (inner instanceof InstanceOfExpressionDefining) {
193+
return ((InstanceOfExpressionDefining) inner).getDefines();
132194
}
133195
return null;
134196
}
135197

198+
/*
199+
* Recognise only the exact canary shape introduced by condenseInstanceOfAssign
200+
* (Literal.NULL on the LHS, target expression on the RHS, op NE).
201+
* Returns the RHS, or null if `op` is not the canary shape.
202+
*/
203+
private static Expression getCanaryRhs(ConditionalExpression op) {
204+
if (!(op instanceof ComparisonOperation)) return null;
205+
ComparisonOperation cmp = (ComparisonOperation) op;
206+
if (cmp.getOp() != CompOp.NE) return null;
207+
if (!Literal.NULL.equals(cmp.getLhs())) return null;
208+
return cmp.getRhs();
209+
}
210+
211+
/*
212+
* `null != b` — the post-j16-lift residue, redundant given a sibling
213+
* `instanceof T b` clause.
214+
*/
215+
private static LValue getNullCheckedLValue(ConditionalExpression op) {
216+
Expression rhs = getCanaryRhs(op);
217+
if (!(rhs instanceof LValueExpression)) return null;
218+
return ((LValueExpression) rhs).getLValue();
219+
}
220+
221+
/*
222+
* `null != (b = (T)o)` — the assignment still embedded in the canary
223+
* because the j16 lift refused (e.g. b is method-scoped).
224+
*/
225+
private static AssignmentExpression getAbsorbedAssignment(ConditionalExpression op) {
226+
Expression rhs = getCanaryRhs(op);
227+
return rhs instanceof AssignmentExpression ? (AssignmentExpression) rhs : null;
228+
}
229+
230+
/*
231+
* The canary always pairs the absorbed `null != (b = (T)o)` with a plain
232+
* `instanceof T` whose subject and type match the cast. We require that
233+
* pairing to be confident this clause came from condenseInstanceOfAssign
234+
* rather than user code where the null-check carries semantic weight.
235+
*/
236+
private static boolean hasMatchingInstanceofSibling(AssignmentExpression absorbed, List<ConditionalExpression> operands) {
237+
if (!(absorbed.getrValue() instanceof CastExpression)) return false;
238+
CastExpression cast = (CastExpression) absorbed.getrValue();
239+
Expression castSubject = cast.getChild();
240+
JavaTypeInstance castType = cast.getInferredJavaType().getJavaTypeInstance();
241+
for (ConditionalExpression op : operands) {
242+
if (!(op instanceof BooleanExpression)) continue;
243+
Expression inner = ((BooleanExpression) op).getInner();
244+
if (!(inner instanceof InstanceOfExpression)) continue;
245+
InstanceOfExpression ioe = (InstanceOfExpression) inner;
246+
if (!ioe.getLhs().equals(castSubject)) continue;
247+
if (!ioe.getTypeInstance().equals(castType)) continue;
248+
return true;
249+
}
250+
return false;
251+
}
136252
}

0 commit comments

Comments
 (0)