*** empty log message ***

git-svn-id: https://svn.code.sf.net/p/jode/code/trunk@40 379699f6-c40d-0410-875b-85095c16579e
stable
jochen 26 years ago
parent 5544356bf9
commit e353bf575c
  1. 6
      jode/jode/Decompiler.java
  2. 131
      jode/jode/decompiler/CodeAnalyzer.java
  3. 9
      jode/jode/decompiler/LocalInfo.java
  4. 5
      jode/jode/expr/ArrayLoadOperator.java
  5. 6
      jode/jode/expr/AssignOperator.java
  6. 10
      jode/jode/expr/BinaryOperator.java
  7. 7
      jode/jode/expr/CompareBinaryOperator.java
  8. 4
      jode/jode/expr/Expression.java
  9. 4
      jode/jode/expr/IfThenElseOperator.java
  10. 4
      jode/jode/expr/Instruction.java
  11. 6
      jode/jode/expr/LocalLoadOperator.java
  12. 7
      jode/jode/expr/LocalStoreOperator.java
  13. 4
      jode/jode/expr/Operator.java
  14. 6
      jode/jode/expr/PostFixOperator.java
  15. 9
      jode/jode/expr/UnaryOperator.java
  16. 8
      jode/jode/flow/BreakBlock.java
  17. 3
      jode/jode/flow/CatchBlock.java
  18. 7
      jode/jode/flow/ConditionalBlock.java
  19. 17
      jode/jode/flow/EmptyBlock.java
  20. 563
      jode/jode/flow/FlowBlock.java
  21. 14
      jode/jode/flow/IfThenElseBlock.java
  22. 10
      jode/jode/flow/InstructionBlock.java
  23. 3
      jode/jode/flow/InstructionContainer.java
  24. 18
      jode/jode/flow/Jump.java
  25. 2
      jode/jode/flow/LoopBlock.java
  26. 10
      jode/jode/flow/RemoveEmpty.java
  27. 8
      jode/jode/flow/ReturnBlock.java
  28. 11
      jode/jode/flow/SequentialBlock.java
  29. 10
      jode/jode/flow/SimplifyExpression.java
  30. 108
      jode/jode/flow/StructuredBlock.java
  31. 23
      jode/jode/flow/VariableSet.java

@ -25,7 +25,9 @@ public class Decompiler {
public static boolean isVerbose = false;
public static boolean isDebugging = false;
public static boolean isTypeDebugging = false;
public static boolean isFlowDebugging = false;
public static boolean showLVT = false;
public static boolean doChecks = false;
public static void main(String[] params) {
JodeEnvironment env = new JodeEnvironment();
@ -36,8 +38,12 @@ public class Decompiler {
isDebugging = true;
else if (params[i].equals("-type"))
isTypeDebugging = true;
else if (params[i].equals("-flow"))
isFlowDebugging = true;
else if (params[i].equals("-lvt"))
showLVT = true;
else if (params[i].equals("-check"))
doChecks = true;
else
env.doClass(params[i]);
}

@ -154,62 +154,99 @@ public class CodeAnalyzer implements Analyzer, Constants {
/* XXX optimize */
Stack todo = new Stack();
FlowBlock flow = methodHeader;
analyzation:
while (true) {
/* First do some non flow transformations. */
int i=0;
while (i < exprTrafos.length) {
if (exprTrafos[i].transform(flow))
i = 0;
else
i++;
try {
jode.TabbedPrintWriter writer = null;
if (Decompiler.isFlowDebugging) {
writer = new jode.TabbedPrintWriter(System.err, " ");
}
analyzation:
while (true) {
if (Decompiler.isFlowDebugging) {
writer.println("before Transformation: ");
writer.tab();
flow.dumpSource(writer);
writer.untab();
}
/* First do some non flow transformations. */
int i=0;
while (i < exprTrafos.length) {
if (exprTrafos[i].transform(flow))
i = 0;
else
i++;
}
if (flow.doT2(todo)) {
/* T2 transformation succeeded. This may
* make another T1 analysis in the previous
* block possible.
*/
if (!todo.isEmpty())
flow = (FlowBlock) todo.pop();
}
if (Decompiler.isFlowDebugging) {
writer.println("after Transformation: ");
writer.tab();
flow.dumpSource(writer);
writer.untab();
}
FlowBlock succ = flow.getSuccessor();
while (succ != null && !flow.doT1(succ)) {
if (flow.doT2(todo)) {
/* T1 transformation failed. */
if (!todo.contains(succ) && succ != flow) {
/* succ wasn't tried before, succeed with
* successor and put flow on the stack.
if (Decompiler.isFlowDebugging) {
writer.println("after T2: ");
writer.tab();
flow.dumpSource(writer);
writer.untab();
}
/* T2 transformation succeeded. This may
* make another T1 analysis in the previous
* block possible.
*/
todo.push(flow);
flow = succ;
continue analyzation;
if (!todo.isEmpty())
flow = (FlowBlock) todo.pop();
}
FlowBlock succ = flow.getSuccessor();
while (true) {
if (succ == null) {
/* the Block has no successor where t1 is applicable.
*
* If everything is okay the stack should be empty now,
* and the program is transformed correctly.
*/
if (todo.isEmpty())
break analyzation;
/* Otherwise pop the last flow block from stack and
* try another successor.
*/
succ = flow;
flow = (FlowBlock) todo.pop();
} else if (flow.doT1(succ)) {
/* T1 transformation succeeded. */
if (Decompiler.isFlowDebugging) {
writer.println("after T1: ");
writer.tab();
flow.dumpSource(writer);
writer.untab();
}
if (Decompiler.isVerbose)
System.err.print(".");
continue analyzation;
} else if (!todo.contains(succ) && succ != flow) {
/* succ wasn't tried before, succeed with
* successor and put flow on the stack.
*/
todo.push(flow);
flow = succ;
continue analyzation;
}
/* Try the next successor.
*/
succ = flow.getSuccessor(succ);
}
if (succ == null) {
/* the Block has no successor where t1 is applicable.
*
* If everything is okay the stack should be empty now,
* and the program is transformed correctly.
*
* Otherwise flow transformation didn't succeeded.
*/
// System.err.println("breaking analyzation; flow: "
// + flow.getLabel());
while (!todo.isEmpty()) {
System.err.println("on Stack: "
+ ((FlowBlock)todo.pop()).getLabel());
/* Try the next successor.
*/
succ = flow.getSuccessor(succ);
}
break analyzation;
}
if (Decompiler.isVerbose)
System.err.print(".");
} catch (java.io.IOException ioex) {
}
}

@ -37,7 +37,6 @@ public class LocalInfo {
private Identifier name;
private Type type;
private LocalInfo shadow;
private jode.flow.StructuredBlock defining;
/* The current implementation may use very much stack. This
* should be changed someday.
@ -156,14 +155,6 @@ public class LocalInfo {
return this.type;
}
public jode.flow.StructuredBlock getDefining() {
return getLocalInfo().defining;
}
public void setDefining(jode.flow.StructuredBlock structuredBlock) {
getLocalInfo().defining = structuredBlock;
}
public boolean isShadow() {
return (shadow != null);
}

@ -40,15 +40,12 @@ public class ArrayLoadOperator extends SimpleOperator {
/**
* Sets the return type of this operator.
* @return true if the operand types changed
*/
public boolean setType(Type type) {
public void setType(Type type) {
if (type != this.type) {
super.setType(type);
operandTypes[0] = Type.tArray(type);
return true;
}
return false;
}
public void setOperandType(Type[] t) {

@ -50,12 +50,10 @@ public class AssignOperator extends Operator {
/**
* Sets the return type of this operator.
* @return true if the operand types changed
*/
public boolean setType(Type type) {
boolean result = store.setLValueType(type);
public void setType(Type type) {
store.setLValueType(type);
super.setType(store.getLValueType());
return result;
}
/**

@ -74,15 +74,9 @@ public class BinaryOperator extends Operator {
/**
* Sets the return type of this operator.
* @return true if the operand types changed
*/
public boolean setType(Type newType) {
operandType = MyType.intersection(operandType, newType);
if (type != operandType) {
type = operandType;
return true;
}
return false;
public void setType(Type newType) {
type = operandType = MyType.intersection(operandType, newType);
}
public boolean equals(Object o) {

@ -44,6 +44,13 @@ public class CompareBinaryOperator extends SimpleOperator {
return getPriority()+i;
}
public void setOperandType(Type[] inputTypes) {
super.setOperandType(inputTypes);
Type operandType =
MyType.intersection(operandTypes[0],operandTypes[1]);
operandTypes[0] = operandTypes[1] = operandType;
}
public boolean equals(Object o) {
return (o instanceof CompareBinaryOperator) &&
((CompareBinaryOperator)o).operator == operator;

@ -134,7 +134,9 @@ public class Expression extends Instruction {
if (operator.getPriority() < minPriority) {
result = "("+result+")";
}
if (operator.getType() == MyType.tError)
if (Decompiler.isTypeDebugging)
result = "(("+operator.getType()+")"+result+")";
else if (operator.getType() == MyType.tError)
result = "(/*type error */" + result+")";
return result;
}

@ -58,14 +58,12 @@ public class IfThenElseOperator extends SimpleOperator {
* Sets the return type of this operator.
* @return true if the operand types changed
*/
public boolean setType(Type newType) {
public void setType(Type newType) {
Type operandType =
MyType.intersection(operandTypes[1], newType);
if (type != operandType) {
type = operandTypes[1] = operandTypes[2] = operandType;
return true;
}
return false;
}
public boolean equals(Object o) {

@ -31,6 +31,10 @@ public abstract class Instruction {
return type;
}
public void setType(Type newType) {
this.type = newType;
}
public Instruction simplify() {
return this;
}

@ -43,7 +43,7 @@ implements LocalVarOperator {
// }
public LocalInfo getLocalInfo() {
return local;
return local.getLocalInfo();
}
public Type getType() {
@ -51,9 +51,9 @@ implements LocalVarOperator {
return local.getType();
}
public boolean setType(Type type) {
public void setType(Type type) {
// System.err.println("LocalLoad.setType of "+local.getName()+": "+local.getType());
return super.setType(local.setType(type));
super.setType(local.setType(type));
}
// public int getSlot() {

@ -37,13 +37,8 @@ implements LocalVarOperator {
return true;
}
// public void setLocalInfo(LocalInfo local) {
// local.setType(lvalueType);
// this.local = local;
// }
public LocalInfo getLocalInfo() {
return local;
return local.getLocalInfo();
}
public Type getLValueType() {

@ -68,11 +68,9 @@ public abstract class Operator extends Instruction {
/**
* Sets the return type of this operator.
* @return true if the operand types changed
*/
public boolean setType(Type type) {
public void setType(Type type) {
this.type = type;
return false;
}
public String getOperatorString() {

@ -53,12 +53,10 @@ public class PostFixOperator extends Operator {
/**
* Sets the return type of this operator.
* @return true if the operand types changed
*/
public boolean setType(Type type) {
boolean result = store.setLValueType(type);
public void setType(Type type) {
store.setLValueType(type);
super.setType(store.getLValueType());
return result;
}
public void setOperandType(Type[] inputTypes) {

@ -35,16 +35,11 @@ public class UnaryOperator extends SimpleOperator {
/**
* Sets the return type of this operator.
* @return true if the operand types changed
*/
public boolean setType(Type type) {
public void setType(Type type) {
super.setType(type);
Type newOpType = MyType.intersection(type, operandTypes[0]);
if (newOpType != operandTypes[0]) {
operandTypes[0] = newOpType;
return true;
}
return false;
operandTypes[0] = newOpType;
}
public boolean equals(Object o) {

@ -63,6 +63,14 @@ public class BreakBlock extends StructuredBlock {
return breaksBlock.getNextFlowBlock();
}
/**
* Tells if this block needs braces when used in a if or while block.
* @return true if this block should be sorrounded by braces.
*/
public boolean needsBraces() {
return false;
}
public void dumpInstruction(TabbedPrintWriter writer)
throws java.io.IOException
{

@ -85,8 +85,7 @@ public class CatchBlock extends StructuredBlock {
? new EmptyBlock()
: catchBlock.getSubBlocks()[1]);
if (catchBlock.jump != null)
newCatchBlock.moveJump(catchBlock);
newCatchBlock.moveJump(catchBlock.jump);
catchBlock = newCatchBlock;
}
}

@ -34,10 +34,9 @@ public class ConditionalBlock extends InstructionContainer {
*/
public ConditionalBlock(Instruction cond, Jump condJump, Jump elseJump) {
super(cond, elseJump);
if (cond instanceof LocalVarOperator) {
LocalVarOperator varOp = (LocalVarOperator) cond;
condJump.out.addElement(varOp.getLocalInfo());
}
/* cond is a CompareBinary or CompareUnary operator, so no
* check for LocalVarOperator (for condJump) is needed here.
*/
trueBlock = new EmptyBlock(condJump);
trueBlock.outer = this;
}

@ -31,6 +31,23 @@ public class EmptyBlock extends StructuredBlock {
setJump(jump);
}
/**
* Appends a block to this block.
* @return the new combined block.
*/
public StructuredBlock appendBlock(StructuredBlock block) {
if (outer instanceof ConditionalBlock) {
IfThenElseBlock ifBlock =
new IfThenElseBlock(((ConditionalBlock)outer).
getInstruction());
ifBlock.replace(outer, this);
ifBlock.moveJump(outer.jump);
ifBlock.setThenBlock(this);
}
block.replace(this, null);
return block;
}
public void dumpInstruction(TabbedPrintWriter writer)
throws java.io.IOException
{

@ -131,7 +131,7 @@ public class FlowBlock {
newBlock.setCatchBlock(catchBlock);
if (sequBlock.jump != null) {
if (newBlock.catchBlock.jump == null)
newBlock.catchBlock.moveJump(sequBlock);
newBlock.catchBlock.moveJump(sequBlock.jump);
else
sequBlock.removeJump();
}
@ -156,28 +156,62 @@ public class FlowBlock {
if (jump == null || jump.destination != successor)
continue next_jump;
same_jump: while(true) {
while(jump != null) {
if (jump.prev instanceof EmptyBlock
&& jump.prev.outer != null
&& jump.prev.outer instanceof ConditionalBlock
&& jump.prev.outer.jump != null) {
if (jump.prev.outer.jump.destination == jump.destination) {
/* This is a weired "if (cond) empty"-block. We
* transform it by hand.
*/
jump.prev.removeJump();
continue next_jump;
}
/* Swap conditional blocks, that have two jumps, and where
* this jump is the inner jump.
*/
StructuredBlock prev = jump.prev;
ConditionalBlock cb = (ConditionalBlock) prev.outer;
jode.Instruction instr = cb.getInstruction();
/* XXX Expression clean up is necessary. Otherwise
* this may lead to a ClassCastException.
*
* Our code below _depends_ on the fact that this
* transformation is done.
*/
cb.setInstruction(((jode.Expression)instr).negate());
cb.swapJump(jump.prev);
}
/* Now move the jump as far to the outer as possible,
* without leaving appendBlock.
*
* Note: jump.prev != appendblock implies
* jump.prev.outer != null, since appendBlock is an outer
* block of jump.prev
*/
while (jump.prev != appendBlock
&& jump.prev.outer.isSingleExit(jump.prev)) {
jump.prev.outer.moveJump(jump);
}
/* if the jump is the jump of the appendBlock, skip it.
*/
if (jump.prev == null || jump.prev == appendBlock)
if (jump.prev == appendBlock)
continue next_jump;
/* Note: jump.prev.outer != null, since appendBlock is
* an outer block of jump.prev
*/
/* remove all jumps to the successor which have the successor
* as getNextFlowBlock().
*/
/* remove this jump if it jumps to the getNextFlowBlock().
*/
if (jump.prev.outer.getNextFlowBlock(jump.prev) == successor) {
jump.prev.removeJump();
continue next_jump;
}
/* replace all conditional jumps to the successor, which
* are followed by a block which has the end of the block
* as normal successor, with "if (not condition) block".
*/
if (jump.prev instanceof EmptyBlock &&
jump.prev.outer instanceof ConditionalBlock) {
@ -185,126 +219,15 @@ public class FlowBlock {
ConditionalBlock cb = (ConditionalBlock) prev.outer;
jode.Instruction instr = cb.getInstruction();
/* cb.jump is null (see above), so cb must have a *
* successor in this block, that means cb.outer is not
* null.
*/
if ((cb == appendBlock ||
cb.outer.getNextFlowBlock(cb) == successor
/*XXX jumpMayBeChanged()??? */) &&
instr instanceof jode.Expression) {
cb.setInstruction(((jode.Expression)instr).negate());
prev.removeJump();
prev.moveJump(cb);
continue next_jump;
}
/* cb.outer is not null,
* since appendBlock outers cb */
if (cb.outer instanceof SequentialBlock &&
cb.outer.getSubBlocks()[0] == cb &&
(cb.outer.getNextFlowBlock() == successor ||
cb.outer.jumpMayBeChanged()) &&
instr instanceof jode.Expression) {
SequentialBlock sequBlock =
(SequentialBlock) cb.outer;
IfThenElseBlock newIfBlock =
new IfThenElseBlock(((jode.Expression)instr).negate());
newIfBlock.replace(sequBlock, sequBlock.getSubBlocks()[1]);
newIfBlock.setThenBlock(sequBlock.getSubBlocks()[1]);
newIfBlock.moveJump(sequBlock);
if (appendBlock == sequBlock)
appendBlock = newIfBlock;
if (newIfBlock.getNextFlowBlock() != successor &&
newIfBlock != appendBlock) {
newIfBlock.moveJump(prev);
continue same_jump;
} else {
prev.removeJump();
continue next_jump;
}
}
}
/* if there are jumps in an if-then block, which
* have as normal successor the end of the if-then block, and
* the if-then block is followed by a single block, then replace
* the if-then block with a if-then-else block and remove the
* unconditional jump.
*/
StructuredBlock elseBlock =
jump.prev.outer.getNextBlock(jump.prev);
if (elseBlock != null
&& elseBlock.outer != null
&& elseBlock.outer instanceof SequentialBlock
&& elseBlock.outer.getSubBlocks()[0] instanceof IfThenElseBlock
&& (elseBlock.outer.getNextFlowBlock() == successor
|| elseBlock.outer.jumpMayBeChanged())) {
IfThenElseBlock ifBlock =
(IfThenElseBlock)elseBlock.outer.getSubBlocks()[0];
if (ifBlock.getSubBlocks().length == 1) {
/* make sure that only sequential blocks are between
* jump.prev and the ifBlock
*/
StructuredBlock block = jump.prev.outer;
while (block instanceof SequentialBlock)
block = block.outer;
if (block == ifBlock) {
elseBlock.outer.removeJump();
ifBlock.replace(elseBlock.outer, elseBlock);
if (appendBlock == elseBlock.outer)
appendBlock = ifBlock;
ifBlock.moveJump(jump.prev);
ifBlock.setElseBlock(elseBlock);
continue same_jump;
}
}
}
/* if the successor is the dummy return instruction, replace all
* jumps with a return.
*/
if (successor == END_OF_METHOD) {
SequentialBlock sequBlock = new SequentialBlock();
StructuredBlock prevBlock = jump.prev;
prevBlock.removeJump();
if (prevBlock instanceof EmptyBlock) {
if (prevBlock.outer instanceof ConditionalBlock) {
IfThenElseBlock ifBlock =
new IfThenElseBlock
(((ConditionalBlock)prevBlock.outer).
getInstruction());
ifBlock.replace(prevBlock.outer, prevBlock);
ifBlock.moveJump(prevBlock.outer);
ifBlock.setThenBlock(prevBlock);
}
new ReturnBlock().replace(prevBlock, null);
} else {
sequBlock.replace(prevBlock, prevBlock);
sequBlock.setFirst(prevBlock);
sequBlock.setSecond(new ReturnBlock());
}
continue next_jump;
}
/* If this is a conditional jump, the first instruction of
* a while and the condition of the while is true, use
* the condition as while condition.
*/
if (jump.prev instanceof EmptyBlock &&
jump.prev.outer instanceof ConditionalBlock &&
jump.prev.outer.jump == null) {
StructuredBlock prev = jump.prev;
ConditionalBlock cb = (ConditionalBlock) prev.outer;
jode.Instruction instr = cb.getInstruction();
/* If this is the first instruction of a while and the
* condition of the while is true, use the condition
* as while condition.
*/
/* This is the first instruction in a while block */
if (cb.outer instanceof SequentialBlock &&
@ -320,10 +243,14 @@ public class FlowBlock {
prev.removeJump();
loopBlock.setCondition(((Expression)instr).negate());
if (cb.outer.jump != null) {
if (cb.outer.getSubBlocks()[1].jump != null)
/* XXX can this happen */
if (cb.outer.getSubBlocks()[1].jump != null) {
/* XXX if above can happen,
* can this happen at all??? */
cb.outer.removeJump();
else
cb.outer.getSubBlocks()[1].moveJump(cb.outer);
} else
cb.outer.getSubBlocks()[1].
moveJump(cb.outer.jump);
}
cb.outer.getSubBlocks()[1].replace
(cb.outer, cb.outer.getSubBlocks()[1]);
@ -354,58 +281,134 @@ public class FlowBlock {
continue next_jump;
}
}
/* replace all conditional jumps to the successor, which
* are followed by a block which has the end of the block
* as normal successor, with "if (not condition) block".
*/
if (cb.outer instanceof SequentialBlock &&
cb.outer.getSubBlocks()[0] == cb &&
(cb.outer.getNextFlowBlock() == successor ||
cb.outer.jumpMayBeChanged()) &&
instr instanceof jode.Expression) {
SequentialBlock sequBlock =
(SequentialBlock) cb.outer;
IfThenElseBlock newIfBlock =
new IfThenElseBlock(((jode.Expression)instr).negate());
newIfBlock.replace(sequBlock, sequBlock.getSubBlocks()[1]);
newIfBlock.setThenBlock(sequBlock.getSubBlocks()[1]);
newIfBlock.moveJump(sequBlock.jump);
if (appendBlock == sequBlock)
appendBlock = newIfBlock;
if (newIfBlock.getNextFlowBlock() != successor &&
newIfBlock != appendBlock) {
newIfBlock.moveJump(jump);
continue;
} else {
prev.removeJump();
continue next_jump;
}
}
}
/* if there are jumps in a while block or switch block and the
* while/switch block is followed by a jump to successor or has
* successor as getNextFlowBlock(), replace jump with break to
* the innermost such while/switch block.
/* if this is a jumps at the end of a then block belonging
* to a if-then block without else part, and the if-then
* block is followed by a single block, then replace the
* if-then block with a if-then-else block and remove the
* unconditional jump.
*/
else if (jump.prev.outer instanceof IfThenElseBlock) {
IfThenElseBlock ifBlock =
(IfThenElseBlock)jump.prev.outer;
if (ifBlock.elseBlock == null
&& ifBlock.jump == null
&& ifBlock.outer instanceof SequentialBlock
&& ifBlock.outer.getSubBlocks()[0] == ifBlock
&& (ifBlock.outer.getNextFlowBlock() == successor
|| ifBlock.outer.jumpMayBeChanged())) {
StructuredBlock elseBlock =
ifBlock.outer.getSubBlocks()[1];
ifBlock.outer.removeJump();
ifBlock.replace(ifBlock.outer, elseBlock);
if (appendBlock == elseBlock.outer)
appendBlock = ifBlock;
ifBlock.moveJump(jump);
ifBlock.setElseBlock(elseBlock);
continue;
}
}
/* if this is a jump in a breakable block, and that block
* has not yet a next block, then create a new jump to that
* successor.
*
* If the switch block hasn't been breaked before we could
* take some heuristics and add a jump after the switch to
* succesor, so that the above succeeds.
* The break to the block will be generated later.
*/
int breaklevel = 0;
for (StructuredBlock surrounder = jump.prev.outer;
surrounder != null && surrounder != appendBlock.outer;
surrounder = surrounder.outer) {
if (surrounder instanceof BreakableBlock) {
breaklevel++;
if (surrounder.getNextFlowBlock() == successor ||
surrounder.jumpMayBeChanged()) {
SequentialBlock sequBlock = new SequentialBlock();
StructuredBlock prevBlock = jump.prev;
if (surrounder.getNextFlowBlock() != successor)
surrounder.moveJump(prevBlock);
else
prevBlock.removeJump();
if (prevBlock instanceof EmptyBlock) {
if (prevBlock.outer instanceof ConditionalBlock) {
IfThenElseBlock ifBlock =
new IfThenElseBlock
(((ConditionalBlock)prevBlock.outer)
.getInstruction());
ifBlock.replace(prevBlock.outer, prevBlock);
ifBlock.moveJump(prevBlock.outer);
ifBlock.setThenBlock(prevBlock);
}
new BreakBlock((BreakableBlock) surrounder,
breaklevel >1
).replace(prevBlock, null);
} else {
sequBlock.replace(prevBlock, prevBlock);
sequBlock.setFirst(prevBlock);
sequBlock.setSecond
(new BreakBlock((BreakableBlock) surrounder,
breaklevel > 1));
}
continue same_jump;
if (surrounder.getNextFlowBlock() != successor
&& surrounder.jumpMayBeChanged()) {
surrounder.setJump(new Jump(successor));
successors.addElement(surrounder.jump);
}
}
}
// /* XXX - There is a problem with this.
// * The here created break could be obsoleted later.
// * We should instead only add the jump to surrounder
// * if necessary and add the break after all jumps
// * has been considered.
// */
// /* if there are jumps in a while block or switch block and the
// * while/switch block is followed by a jump to successor or has
// * successor as getNextFlowBlock(), replace jump with break to
// * the innermost such while/switch block.
// *
// * If the switch block hasn't been breaked before we could
// * take some heuristics and add a jump after the switch to
// * succesor, so that the above succeeds.
// */
// int breaklevel = 0;
// for (StructuredBlock surrounder = jump.prev.outer;
// surrounder != null && surrounder != appendBlock.outer;
// surrounder = surrounder.outer) {
// if (surrounder instanceof BreakableBlock) {
// breaklevel++;
// if (surrounder.getNextFlowBlock() == successor ||
// surrounder.jumpMayBeChanged()) {
// SequentialBlock sequBlock = new SequentialBlock();
// StructuredBlock prevBlock = jump.prev;
// if (surrounder.getNextFlowBlock() != successor)
// surrounder.moveJump(prevBlock.jump);
// else {
// prevBlock.removeJump();
// jump = null;
// }
// prevBlock.appendBlock
// (new BreakBlock((BreakableBlock) surrounder,
// breaklevel >1));
// continue;
// }
// }
// }
continue next_jump;
}
}
@ -419,23 +422,23 @@ public class FlowBlock {
* block.
* @return The variables that must be defined in this block.
*/
VariableSet updateInOut (FlowBlock successor, boolean t1Transformation) {
void updateInOut (FlowBlock successor, boolean t1Transformation) {
/* First get the out vectors of all jumps to successor and
* calculate the intersection.
*/
VariableSet allOuts = new VariableSet();
VariableSet intersectOut = null;
VariableSet gens = new VariableSet();
VariableSet kills = null;
Enumeration enum = successors.elements();
while (enum.hasMoreElements()) {
Jump jump = (Jump) enum.nextElement();
if (jump == null || jump.destination != successor)
continue;
allOuts.union(jump.out);
if (intersectOut == null)
intersectOut = jump.out;
gens.unionExact(jump.gen);
if (kills == null)
kills = jump.kill;
else
intersectOut = intersectOut.intersect(jump.out);
kills = kills.intersect(jump.kill);
}
// System.err.println("UpdateInOut: allOuts : "+allOuts);
@ -444,26 +447,25 @@ public class FlowBlock {
/* Merge the locals used in successing block with those written
* by this blocks
*/
VariableSet defineHere = successor.in.merge(allOuts);
defineHere.subtractExact(in);
successor.in.merge(gens);
// System.err.println(" defineHere : "+defineHere);
if (t1Transformation) {
/* Now update in and out set of successing block */
successor.in.subtract(intersectOut);
/* The out set must be updated for every jump in the block */
enum = successor.successors.elements();
while (enum.hasMoreElements()) {
Jump jump = (Jump) enum.nextElement();
if (jump != null)
jump.out.add(intersectOut);
/* Now update in and out set of successing block */
if (t1Transformation)
successor.in.subtract(kills);
/* The gen/kill sets must be updated for every jump in the block */
enum = successor.successors.elements();
while (enum.hasMoreElements()) {
Jump jump = (Jump) enum.nextElement();
if (jump != null) {
jump.gen.mergeGenKill(gens, jump.kill);
if (t1Transformation)
jump.kill.add(kills);
}
}
// System.err.println(" successor.in: "+successor.in);
in.union(successor.in);
in.unionExact(successor.in);
// System.err.println(" in : "+in);
/* XXX - do something with defineHere */
return defineHere; /*XXX - correct???*/
}
@ -566,6 +568,8 @@ public class FlowBlock {
}
public void checkConsistent() {
if (!jode.Decompiler.doChecks)
return;
if (block.outer != null || block.flowBlock != this) {
throw new RuntimeException("Inconsistency");
}
@ -611,11 +615,7 @@ public class FlowBlock {
return false;
try{
// System.err.println("doing T1 analysis on: "+getLabel());
// System.err.println("***in: "+in);
checkConsistent();
// System.err.println("and "+succ.getLabel());
// System.err.println("+++in: "+succ.in);
succ.checkConsistent();
} catch (RuntimeException ex) {
ex.printStackTrace();
@ -624,7 +624,7 @@ public class FlowBlock {
new jode.TabbedPrintWriter(System.err, " ");
writer.tab();
block.dumpSource(writer);
succ.block.dumpSource(writer);
succ.block.dumpSource(writer);
} catch (java.io.IOException ioex) {
}
}
@ -658,7 +658,7 @@ public class FlowBlock {
}
/* Update the in/out-Vectors now */
VariableSet defineHere = updateInOut(succ, true);
updateInOut(succ, true);
/* The switch "fall through" case: if the appendBlock is a
* switch, and the successor is the address of a case, and all
@ -669,7 +669,7 @@ public class FlowBlock {
if (appendBlock instanceof SwitchBlock) {
nextcase = ((SwitchBlock) appendBlock).findCase(succ);
precedingcase =
((SwitchBlock) appendBlock).prevCase(precedingcase);
((SwitchBlock) appendBlock).prevCase(nextcase);
enum = successors.elements();
while (nextcase != null && enum.hasMoreElements()) {
@ -680,8 +680,7 @@ public class FlowBlock {
|| (precedingcase != null
&& precedingcase.contains(jump.prev)))
continue;
nextcase = null;
nextcase = null;
}
}
if (succ == END_OF_METHOD) {
@ -696,8 +695,6 @@ public class FlowBlock {
/* Do the following modifications on the struct block. */
appendBlock = precedingcase;
succ.block.setFlowBlock(this);
// switchBlock.define(defineHere);
} else {
@ -723,8 +720,6 @@ public class FlowBlock {
sequBlock.replace(appendBlock, appendBlock);
sequBlock.setFirst(appendBlock);
sequBlock.setSecond(succ.block);
succ.block.setFlowBlock(this);
// sequBlock.define(defineHere);
}
/* Merge the sucessors from the successing flow block
@ -743,89 +738,108 @@ public class FlowBlock {
}
}
try {
// System.err.println("before optimizeJump: "+getLabel());
checkConsistent();
} catch (RuntimeException ex) {
ex.printStackTrace();
try {
jode.TabbedPrintWriter writer =
new jode.TabbedPrintWriter(System.err, " ");
writer.tab();
checkConsistent();
} catch (RuntimeException ex) {
ex.printStackTrace();
try {
jode.TabbedPrintWriter writer =
new jode.TabbedPrintWriter(System.err, " ");
writer.tab();
block.dumpSource(writer);
} catch (java.io.IOException ioex) {
} catch (java.io.IOException ioex) {
}
}
}
/* Try to eliminate as many jumps as possible.
*/
appendBlock = optimizeJumps(succ, appendBlock);
try {
// System.err.println("after optimizeJump: "+getLabel());
checkConsistent();
} catch (RuntimeException ex) {
ex.printStackTrace();
try {
jode.TabbedPrintWriter writer =
new jode.TabbedPrintWriter(System.err, " ");
writer.tab();
block.dumpSource(writer);
} catch (java.io.IOException ioex) {
}
}
/* appendBlock may be zero, if this is the switchcase with
* precedingcase = null. But in this case, there can't be
* any jumps.
*/
if (appendBlock != null) {
/* Now remove the jump of the appendBlock if it points to
* successor.
*/
if (appendBlock.jump != null
&& appendBlock.jump.destination == succ)
appendBlock.removeJump();
/* If there are further jumps, put a do/while(0) block around
* appendBlock and replace every remaining jump with a break
* to the do/while block.
*/
appendBlock = optimizeJumps(succ, appendBlock);
try {
checkConsistent();
} catch (RuntimeException ex) {
ex.printStackTrace();
try {
jode.TabbedPrintWriter writer =
new jode.TabbedPrintWriter(System.err, " ");
writer.tab();
block.dumpSource(writer);
} catch (java.io.IOException ioex) {
}
}
LoopBlock doWhileFalse = null;
enum = successors.elements();
next_jump:
while (enum.hasMoreElements()) {
Jump jump = (Jump) enum.nextElement();
if (jump == null || jump.destination != succ)
if (jump == null || jump.destination != succ ||
jump.prev == appendBlock)
continue;
if (doWhileFalse == null)
doWhileFalse = new LoopBlock(LoopBlock.DOWHILE,
LoopBlock.FALSE);
int breaklevel = 1;
for (StructuredBlock surrounder = jump.prev.outer;
surrounder != appendBlock.outer;
surrounder = surrounder.outer) {
if (surrounder instanceof BreakableBlock) {
breaklevel++;
}
}
SequentialBlock sequBlock = new SequentialBlock();
StructuredBlock prevBlock = jump.prev;
prevBlock.removeJump();
sequBlock.replace(prevBlock, prevBlock);
sequBlock.setFirst(prevBlock);
sequBlock.setSecond(new BreakBlock(doWhileFalse, breaklevel > 1));
int breaklevel = 0;
BreakableBlock breakToBlock = null;
for (StructuredBlock surrounder = jump.prev.outer;
surrounder != null && surrounder != appendBlock.outer;
surrounder = surrounder.outer) {
if (surrounder instanceof BreakableBlock) {
breaklevel++;
if (surrounder.getNextFlowBlock() == succ) {
breakToBlock = (BreakableBlock) surrounder;
break;
}
/* We don't want labeled breaks, if we can
* simply return. */
if (succ == END_OF_METHOD)
break;
}
}
StructuredBlock prevBlock = jump.prev;
prevBlock.removeJump();
if (breakToBlock == null) {
/* if the successor is the dummy return instruction
* and no simple breakToBlock could be found above,
* replace the jump with a return.
*/
if (succ == END_OF_METHOD && breakToBlock == null)
prevBlock.appendBlock(new ReturnBlock());
else {
/* Nothing else helped, so put a do/while(0)
* block around appendBlock and break to that
* block.
*/
if (doWhileFalse == null)
doWhileFalse = new LoopBlock(LoopBlock.DOWHILE,
LoopBlock.FALSE);
prevBlock.appendBlock
(new BreakBlock(doWhileFalse, breaklevel > 0));
}
} else
prevBlock.appendBlock
(new BreakBlock(breakToBlock, breaklevel > 1));
}
if (doWhileFalse != null) {
doWhileFalse.replace(appendBlock, appendBlock);
doWhileFalse.setBody(appendBlock);
}
/* Now remove the jump of the appendBlock if it points to
* successor.
*/
if (appendBlock.jump != null
&& appendBlock.jump.destination == succ)
appendBlock.removeJump();
}
/* Believe it or not: Now the rule, that the first part of a
@ -899,7 +913,7 @@ public class FlowBlock {
}
/* Update the in/out-Vectors now */
VariableSet defineHere = updateInOut(this, false);
updateInOut(this, false);
while (lastModified != block) {
@ -945,7 +959,6 @@ public class FlowBlock {
whileBlock.replace(bodyBlock, bodyBlock);
whileBlock.setBody(bodyBlock);
// whileBlock.define(defineHere);
/* Try to eliminate as many jumps as possible.
*/
@ -1069,13 +1082,13 @@ public class FlowBlock {
}
if (!in.isEmpty()) {
writer.print("in: "+in);
writer.println("in: "+in);
}
block.dumpSource(writer);
FlowBlock succ = getSuccessor();
if (succ != null)
succ.dumpSource(writer);
// FlowBlock succ = getSuccessor();
// if (succ != null)
// succ.dumpSource(writer);
}
/**

@ -96,7 +96,7 @@ public class IfThenElseBlock extends StructuredBlock {
public void dumpInstruction(TabbedPrintWriter writer)
throws java.io.IOException
{
boolean needBrace = ! (thenBlock instanceof InstructionBlock);
boolean needBrace = thenBlock.needsBraces();
writer.println("if ("+cond.toString()+")"+(needBrace?" {":""));
writer.tab();
thenBlock.dumpSource(writer);
@ -109,7 +109,7 @@ public class IfThenElseBlock extends StructuredBlock {
writer.print("else ");
elseBlock.dumpSource(writer);
} else {
needBrace = ! (elseBlock instanceof InstructionBlock);
needBrace = elseBlock.needsBraces();
writer.println("else" + (needBrace ? " {" : ""));
writer.tab();
elseBlock.dumpSource(writer);
@ -139,12 +139,8 @@ public class IfThenElseBlock extends StructuredBlock {
* @return true, if the jump may be safely changed.
*/
public boolean jumpMayBeChanged() {
if (thenBlock.jump == null && !thenBlock.jumpMayBeChanged())
return false;
if (elseBlock != null && elseBlock.jump == null &&
!elseBlock.jumpMayBeChanged())
return false;
return true;
return (thenBlock.jump != null || thenBlock.jumpMayBeChanged())
&& elseBlock != null
&& (elseBlock.jump != null || elseBlock.jumpMayBeChanged());
}
}

@ -31,6 +31,14 @@ public class InstructionBlock extends InstructionContainer {
super(instr, jump);
}
/**
* Tells if this block needs braces when used in a if or while block.
* @return true if this block should be sorrounded by braces.
*/
public boolean needsBraces() {
return declare != null && !declare.isEmpty();
}
/**
* True if this is a declaration.
*/
@ -42,7 +50,7 @@ public class InstructionBlock extends InstructionContainer {
if (instr instanceof Expression
&& ((Expression)instr).getOperator() instanceof LocalStoreOperator
&& ((LocalStoreOperator) ((Expression)instr).getOperator())
.getLocalInfo() == local) {
.getLocalInfo() == local.getLocalInfo()) {
isDeclaration = true;
} else
super.dumpDeclaration(writer, local);

@ -36,7 +36,8 @@ public abstract class InstructionContainer extends StructuredBlock {
if (instr instanceof LocalVarOperator) {
LocalVarOperator varOp = (LocalVarOperator) instr;
used.addElement(varOp.getLocalInfo());
jump.out.addElement(varOp.getLocalInfo());
jump.gen.addElement(varOp.getLocalInfo());
jump.kill.addElement(varOp.getLocalInfo());
}
setJump(jump);
}

@ -36,12 +36,21 @@ public class Jump {
int destAddr;
/**
* The out locals. This are the locals, which must be overwritten
* The kill locals. This are the slots, which must be overwritten
* in this block on every path to this jump. That means, that all
* paths form the start of the current flow block to this jump
* contain (unconditional) assignments to this local.
* contain (unconditional) assignments to this slot.
*/
VariableSet out = new VariableSet();
VariableSet kill = new VariableSet();
/**
* The gen locals. This are the locals, which can be overwritten
* in this block on a path to this jump. That means, that there
* exists a path form the start of the current flow block to this
* jump that contains an (unconditional) assignments to this
* local, and that is not overwritten afterwards.
*/
VariableSet gen = new VariableSet();
public Jump (int destAddr) {
this.destAddr = destAddr;
@ -75,7 +84,8 @@ public class Jump {
throws java.io.IOException
{
if (jode.Decompiler.isDebugging) {
writer.println("out: "+ out.toString());
writer.println("gen : "+ gen.toString());
writer.println("kill: "+ kill.toString());
}
writer.println("Attachments: "+describeAttachments());
if (destination == null)

@ -135,7 +135,7 @@ public class LoopBlock extends StructuredBlock implements BreakableBlock {
writer.println(label+":");
writer.tab();
}
boolean needBrace = ! (bodyBlock instanceof InstructionBlock);
boolean needBrace = bodyBlock.needsBraces();
switch (type) {
case WHILE:
writer.print("while ("+cond.toString()+")");

@ -33,8 +33,9 @@ public class RemoveEmpty implements Transformation {
Instruction instr;
try {
block = flow.lastModified;
if (!(((InstructionContainer)block).getInstruction()
instanceof NopOperator))
Instruction prevInstr =
((InstructionContainer)block).getInstruction();
if (!(prevInstr instanceof NopOperator))
return false;
sequBlock = (SequentialBlock)block.outer;
@ -46,6 +47,8 @@ public class RemoveEmpty implements Transformation {
if (prev.jump != null)
return false;
instr = (Instruction) prev.getInstruction();
instr.setType(jode.MyType.intersection
(instr.getType(), prevInstr.getType()));
} catch (NullPointerException ex) {
return false;
} catch (ClassCastException ex) {
@ -66,7 +69,8 @@ public class RemoveEmpty implements Transformation {
StructuredBlock block = lastBlock.outer.getSubBlocks()[0];
block.replace(block.outer, block);
if (block.jump == null)
block.moveJump(lastBlock);
/*XXX can this happen */
block.moveJump(lastBlock.jump);
else
lastBlock.removeJump();
flow.lastModified = block;

@ -34,6 +34,14 @@ public class ReturnBlock extends InstructionContainer {
super(instr);
}
/**
* Tells if this block needs braces when used in a if or while block.
* @return true if this block should be sorrounded by braces.
*/
public boolean needsBraces() {
return false;
}
/**
* Get the underlying instruction.
* @return the underlying instruction.

@ -62,6 +62,16 @@ public class SequentialBlock extends StructuredBlock {
return null;
return getNextFlowBlock();
}
/**
* Tells if the sub block is the single exit point of the current block.
* @param subBlock the sub block.
* @return true, if the sub block is the single exit point of the
* current block.
*/
public boolean isSingleExit(StructuredBlock subBlock) {
return (subBlock == subBlocks[1]);
}
/**
* Make the declarations, i.e. initialize the declare variable
@ -74,6 +84,7 @@ public class SequentialBlock extends StructuredBlock {
* any local Variable, but let the first sub block do this.
*/
declare = new VariableSet();
subBlocks[0].used.addExact(used);
subBlocks[0].makeDeclaration(done);
done.addExact(used);
subBlocks[1].makeDeclaration(done);

@ -21,10 +21,20 @@ package jode.flow;
public class SimplifyExpression implements Transformation {
public boolean transform(FlowBlock flow) {
// try {
// jode.TabbedPrintWriter writer =
// new jode.TabbedPrintWriter(System.err, " ");
// System.out.println("Transforming: ");
// flow.lastModified.dumpSource(writer);
if (flow.lastModified instanceof InstructionContainer) {
InstructionContainer ic = (InstructionContainer) flow.lastModified;
ic.setInstruction(ic.getInstruction().simplify());
}
// System.out.println("Result: ");
// flow.lastModified.dumpSource(writer);
// } catch (java.io.IOException ex) {
// }
return false;
}
}

@ -150,6 +150,16 @@ public abstract class StructuredBlock {
return getNextFlowBlock();
}
/**
* Tells if the sub block is the single exit point of the current block.
* @param subBlock the sub block.
* @return true, if the sub block is the single exit point of the
* current block.
*/
public boolean isSingleExit(StructuredBlock subBlock) {
return false;
}
/**
* Replaces the given sub block with a new block.
* @param oldBlock the old sub block.
@ -179,12 +189,9 @@ public abstract class StructuredBlock {
return (child == this);
}
/**
* Removes the given jump if it is our jump. This does also
* update the successors vector of the flow block.
*
* @param jump The jump that should be removed.
*/
/**
* Removes the jump. This does also update the successors vector
* of the flow block. */
public void removeJump() {
if (jump != null) {
jump.prev = null;
@ -211,8 +218,8 @@ public abstract class StructuredBlock {
while (enum.hasMoreElements()) {
LocalInfo var =
((LocalInfo) enum.nextElement()).getLocalInfo();
used.addElement(var);
var.setDefining(this);
if (!used.contains(var))
used.addElement(var);
}
from.used.removeAllElements();
StructuredBlock[] subs = from.getSubBlocks();
@ -236,8 +243,7 @@ public abstract class StructuredBlock {
public void replace(StructuredBlock sb, StructuredBlock sub) {
moveDefinitions(sb, sub);
outer = sb.outer;
flowBlock = sb.flowBlock;
setFlowBlock(sb.flowBlock);
if (outer != null) {
outer.replaceSubBlock(sb, this);
} else {
@ -246,18 +252,45 @@ public abstract class StructuredBlock {
}
/**
* This function moves the jumps from sb to this block.
* The jump field of sb is removed afterwards.
* @param sb The structured block whose jump is copied.
* This function swaps the jump with another block.
* @param block The block whose jump is swapped.
*/
public void swapJump(StructuredBlock block) {
Jump tmp = block.jump;
block.jump = jump;
jump = tmp;
jump.prev = this;
block.jump.prev = block;
}
/**
* This function moves the jump to this block.
* The jump field of the previous owner is cleared afterwards.
* If the given jump is null, nothing bad happens.
* @param jump The jump that should be moved, may be null.
*/
public void moveJump(StructuredBlock sb) {
jump = sb.jump;
public void moveJump(Jump jump) {
removeJump();
this.jump = jump;
if (jump != null) {
jump.prev.jump = null;
jump.prev = this;
sb.jump = null;
}
}
/**
* Appends a block to this block.
* @return the new combined block.
*/
public StructuredBlock appendBlock(StructuredBlock block) {
SequentialBlock sequBlock = new SequentialBlock();
sequBlock.replace(this, this);
sequBlock.setFirst(this);
sequBlock.setSecond(block);
return sequBlock;
}
/**
* Determines if there is a sub block, that flows through to the end
* of this block. If this returns true, you know that jump is null.
@ -277,18 +310,14 @@ public abstract class StructuredBlock {
public VariableSet propagateUsage() {
StructuredBlock[] subs = getSubBlocks();
VariableSet[] childUse = new VariableSet[subs.length];
VariableSet allUse = (VariableSet) used.clone();
for (int i=0; i<subs.length; i++) {
childUse[i] = subs[i].propagateUsage();
allUse.addExact(childUse[i]);
}
if (subs.length == 2) {
/* All variables used in both sub blocks, are used in
* this block, too.
*/
VariableSet newUse = childUse[0].intersectExact(childUse[1]);
used.addExact(newUse);
VariableSet childUse = subs[i].propagateUsage();
/* All variables used in more than one sub blocks, are
* used in this block, too.
*/
used.addExact(allUse.intersectExact(childUse));
allUse.addExact(childUse);
}
return allUse;
}
@ -341,11 +370,20 @@ public abstract class StructuredBlock {
this.flowBlock = flowBlock;
StructuredBlock[] subs = getSubBlocks();
for (int i=0; i<subs.length; i++) {
subs[i].setFlowBlock(flowBlock);
if (subs[i] != null)
subs[i].setFlowBlock(flowBlock);
}
}
}
/**
* Tells if this block needs braces when used in a if or while block.
* @return true if this block should be sorrounded by braces.
*/
public boolean needsBraces() {
return true;
}
/**
* Fill all in variables into the given VariableSet.
* @param in The VariableSet, the in variables should be stored to.
@ -377,13 +415,17 @@ public abstract class StructuredBlock {
public void dumpSource(jode.TabbedPrintWriter writer)
throws java.io.IOException
{
if (jode.Decompiler.isDebugging)
writer.println("declaring: "+declare);
if (declare != null) {
if (jode.Decompiler.isDebugging) {
writer.println("declaring: "+declare);
writer.println("using: "+used);
}
java.util.Enumeration enum = declare.elements();
while (enum.hasMoreElements()) {
LocalInfo local = (LocalInfo) enum.nextElement();
dumpDeclaration(writer, local);
java.util.Enumeration enum = declare.elements();
while (enum.hasMoreElements()) {
LocalInfo local = (LocalInfo) enum.nextElement();
dumpDeclaration(writer, local);
}
}
dumpInstruction(writer);

@ -116,7 +116,7 @@ public class VariableSet extends java.util.Vector {
/**
* Union the other variable set to the current.
*/
public void union(VariableSet vs) {
public void unionExact(VariableSet vs) {
int oldSize = elementCount;
iloop:
for (int i=0; i< vs.elementCount; i++) {
@ -172,6 +172,27 @@ public class VariableSet extends java.util.Vector {
}
}
/**
* Add the variables in gen to the current set, unless there are
* variables in kill using the same slot.
* @param gen The gen set.
* @param kill The kill set.
*/
public void mergeGenKill(VariableSet gen, VariableSet kill) {
iloop:
for (int i=0; i< gen.elementCount; i++) {
LocalInfo li2 = ((LocalInfo) gen.elementData[i]).getLocalInfo();
/* check if this slot was already overwritten (kill set) */
for (int j=0; j< kill.elementCount; j++) {
LocalInfo li1 = (LocalInfo) kill.elementData[j];
if (li2.getSlot() == li1.getSlot())
/* Yes it was, take next variable */
continue iloop;
}
addElement(li2);
}
}
/**
* Subtract the other variable set from this one. This removes
* every variable from this set, that uses a slot in the other

Loading…
Cancel
Save