diff options
Diffstat (limited to 'chapter09/src')
40 files changed, 1239 insertions, 1240 deletions
diff --git a/chapter09/src/main/java/com/seaofnodes/simple/GraphEvaluator.java b/chapter09/src/main/java/com/seaofnodes/simple/GraphEvaluator.java index c4b349bd..9fc51c8e 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/GraphEvaluator.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/GraphEvaluator.java @@ -1,181 +1,181 @@ -package com.seaofnodes.simple;
-
-import com.seaofnodes.simple.node.*;
-import com.seaofnodes.simple.type.TypeInteger;
-
-import java.util.Arrays;
-import java.util.BitSet;
-import java.util.HashMap;
-
-public class GraphEvaluator {
-
- public enum ResultType { VALUE, FALLTHROUGH, TIMEOUT }
-
- public record Result(ResultType type, long value) {}
-
- /**
- * Find the start node from some node in the graph or null if there is no start node
- */
- private static StartNode findStart(BitSet visit, Node node) {
- if (node == null) return null;
- if (node instanceof StartNode start) return start;
- if( visit.get(node._nid) ) return null;
- visit.set(node._nid);
- for( Node def : node._inputs ) {
- var res = findStart(visit,def);
- if( res != null ) return res;
- }
- for( Node use : node._outputs ) {
- var res = findStart(visit,use);
- if( res != null ) return res;
- }
- return null;
- }
-
- /**
- * Find the control output from a control node
- */
- private static Node findControl(Node control) {
- for( Node use : control._outputs ) {
- if (use.isCFG()) return use;
- }
- return null;
- }
-
- /**
- * Find the projection for a node
- */
- private static ProjNode findProjection(Node node, int idx) {
- for( Node use : node._outputs ) {
- if (use instanceof ProjNode proj && proj._idx == idx) return proj;
- }
- return null;
- }
-
- /**
- * Cache values for phi and parameter projection nodes.
- */
- private final HashMap<Node, Long> cacheValues = new HashMap<>();
- /**
- * Cache for loop phis as they can depend on itself or other loop phis
- */
- private long[] loopPhiCache = new long[16];
-
- private GraphEvaluator() {
- }
-
- private long div(DivNode div) {
- long in2 = getValue(div.in(2));
- return in2 == 0 ? 0 : getValue(div.in(1)) / in2;
- }
-
- /**
- * Calculate the value of a node
- */
- private long getValue(Node node) {
- var cache = cacheValues.get(node);
- if (cache != null) return cache;
- return switch (node) {
- case ConstantNode cons -> ((TypeInteger)cons.compute()).value();
- case AddNode add -> getValue(add.in(1)) + getValue(add.in(2));
- case BoolNode.EQ eq -> getValue(eq.in(1)) == getValue(eq.in(2)) ? 1 : 0;
- case BoolNode.LE le -> getValue(le.in(1)) <= getValue(le.in(2)) ? 1 : 0;
- case BoolNode.LT lt -> getValue(lt.in(1)) < getValue(lt.in(2)) ? 1 : 0;
- case DivNode div -> div(div);
- case MinusNode minus -> -getValue(minus.in(1));
- case MulNode mul -> getValue(mul.in(1)) * getValue(mul.in(2));
- case NotNode not -> getValue(not.in(1)) == 0 ? 1 : 0;
- case SubNode sub -> getValue(sub.in(1)) - getValue(sub.in(2));
- default -> throw Utils.TODO("Unexpected node " + node);
- };
- }
-
- /**
- * Special case of latchPhis when phis can depend on phis of the same region.
- */
- private void latchLoopPhis(RegionNode region, Node prev) {
- int idx = Utils.find(region._inputs, prev);
- assert idx > 0;
- int i = 0;
- for( Node use : region._outputs ) {
- if (use instanceof PhiNode phi) {
- var value = getValue(phi.in(idx));
- if (i == loopPhiCache.length) loopPhiCache = Arrays.copyOf(loopPhiCache, loopPhiCache.length*2);
- loopPhiCache[i++] = value;
- }
- }
- i = 0;
- for( Node use : region._outputs ) {
- if (use instanceof PhiNode phi) cacheValues.put(phi, loopPhiCache[i++]);
- }
- }
-
- /**
- * Calculate the values of phis of the region and caches the values. The phis are not allowed to depend on other phis of the region.
- */
- private void latchPhis(RegionNode region, Node prev) {
- int idx = Utils.find(region._inputs, prev);
- assert idx > 0;
- for( Node use : region._outputs ) {
- if (use instanceof PhiNode phi) {
- var value = getValue(phi.in(idx));
- cacheValues.put(phi, value);
- }
- }
- }
-
- /**
- * Run the graph until either a return is found or the number of loop iterations are done.
- */
- private Result evaluate(StartNode start, long parameter, int loops) {
- var parameter1 = findProjection(start, 1);
- if (parameter1 != null) cacheValues.put(parameter1, parameter);
- Node control = findProjection(start, 0);
- Node prev = start;
- while (control != null) {
- Node next;
- switch (control) {
- case RegionNode region -> {
- if (region instanceof LoopNode && region.in(1) != prev) {
- if (loops--<=0) return new Result(ResultType.TIMEOUT, 0);
- latchLoopPhis(region, prev);
- } else {
- latchPhis(region, prev);
- }
- next = findControl(region);
- }
- case IfNode cond -> next = findProjection(cond, getValue(cond.in(1)) != 0 ? 0 : 1);
- case ReturnNode ret -> {
- return new Result(ResultType.VALUE, getValue(ret.in(1)));
- }
- case ProjNode ignored -> next = findControl(control);
- default -> throw Utils.TODO("Unexpected control node " + control);
- }
- prev = control;
- control = next;
- }
- return new Result(ResultType.FALLTHROUGH, 0);
- }
-
- public static long evaluate(Node graph) {
- return evaluate(graph, 0);
- }
-
- public static long evaluate(Node graph, long parameter) {
- return evaluate(graph, parameter, 1000);
- }
-
- public static long evaluate(Node graph, long parameter, int loops) {
- var res = evaluateWithResult(graph, parameter, loops);
- if (res.type== ResultType.TIMEOUT) throw new RuntimeException("Timeout");
- return res.value;
- }
-
- public static Result evaluateWithResult(Node graph, long parameter, int loops) {
- var start = findStart(new BitSet(), graph);
- if (start == null) return new Result(ResultType.TIMEOUT, 0);
- var evaluator = new GraphEvaluator();
- return evaluator.evaluate(start, parameter, loops);
- }
-
-}
+package com.seaofnodes.simple; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; + +import java.util.Arrays; +import java.util.BitSet; +import java.util.HashMap; + +public class GraphEvaluator { + + public enum ResultType { VALUE, FALLTHROUGH, TIMEOUT } + + public record Result(ResultType type, long value) {} + + /** + * Find the start node from some node in the graph or null if there is no start node + */ + private static StartNode findStart(BitSet visit, Node node) { + if (node == null) return null; + if (node instanceof StartNode start) return start; + if( visit.get(node._nid) ) return null; + visit.set(node._nid); + for( Node def : node._inputs ) { + var res = findStart(visit,def); + if( res != null ) return res; + } + for( Node use : node._outputs ) { + var res = findStart(visit,use); + if( res != null ) return res; + } + return null; + } + + /** + * Find the control output from a control node + */ + private static Node findControl(Node control) { + for( Node use : control._outputs ) { + if (use.isCFG()) return use; + } + return null; + } + + /** + * Find the projection for a node + */ + private static ProjNode findProjection(Node node, int idx) { + for( Node use : node._outputs ) { + if (use instanceof ProjNode proj && proj._idx == idx) return proj; + } + return null; + } + + /** + * Cache values for phi and parameter projection nodes. + */ + private final HashMap<Node, Long> cacheValues = new HashMap<>(); + /** + * Cache for loop phis as they can depend on itself or other loop phis + */ + private long[] loopPhiCache = new long[16]; + + private GraphEvaluator() { + } + + private long div(DivNode div) { + long in2 = getValue(div.in(2)); + return in2 == 0 ? 0 : getValue(div.in(1)) / in2; + } + + /** + * Calculate the value of a node + */ + private long getValue(Node node) { + var cache = cacheValues.get(node); + if (cache != null) return cache; + return switch (node) { + case ConstantNode cons -> ((TypeInteger)cons.compute()).value(); + case AddNode add -> getValue(add.in(1)) + getValue(add.in(2)); + case BoolNode.EQ eq -> getValue(eq.in(1)) == getValue(eq.in(2)) ? 1 : 0; + case BoolNode.LE le -> getValue(le.in(1)) <= getValue(le.in(2)) ? 1 : 0; + case BoolNode.LT lt -> getValue(lt.in(1)) < getValue(lt.in(2)) ? 1 : 0; + case DivNode div -> div(div); + case MinusNode minus -> -getValue(minus.in(1)); + case MulNode mul -> getValue(mul.in(1)) * getValue(mul.in(2)); + case NotNode not -> getValue(not.in(1)) == 0 ? 1 : 0; + case SubNode sub -> getValue(sub.in(1)) - getValue(sub.in(2)); + default -> throw Utils.TODO("Unexpected node " + node); + }; + } + + /** + * Special case of latchPhis when phis can depend on phis of the same region. + */ + private void latchLoopPhis(RegionNode region, Node prev) { + int idx = Utils.find(region._inputs, prev); + assert idx > 0; + int i = 0; + for( Node use : region._outputs ) { + if (use instanceof PhiNode phi) { + var value = getValue(phi.in(idx)); + if (i == loopPhiCache.length) loopPhiCache = Arrays.copyOf(loopPhiCache, loopPhiCache.length*2); + loopPhiCache[i++] = value; + } + } + i = 0; + for( Node use : region._outputs ) { + if (use instanceof PhiNode phi) cacheValues.put(phi, loopPhiCache[i++]); + } + } + + /** + * Calculate the values of phis of the region and caches the values. The phis are not allowed to depend on other phis of the region. + */ + private void latchPhis(RegionNode region, Node prev) { + int idx = Utils.find(region._inputs, prev); + assert idx > 0; + for( Node use : region._outputs ) { + if (use instanceof PhiNode phi) { + var value = getValue(phi.in(idx)); + cacheValues.put(phi, value); + } + } + } + + /** + * Run the graph until either a return is found or the number of loop iterations are done. + */ + private Result evaluate(StartNode start, long parameter, int loops) { + var parameter1 = findProjection(start, 1); + if (parameter1 != null) cacheValues.put(parameter1, parameter); + Node control = findProjection(start, 0); + Node prev = start; + while (control != null) { + Node next; + switch (control) { + case RegionNode region -> { + if (region instanceof LoopNode && region.in(1) != prev) { + if (loops--<=0) return new Result(ResultType.TIMEOUT, 0); + latchLoopPhis(region, prev); + } else { + latchPhis(region, prev); + } + next = findControl(region); + } + case IfNode cond -> next = findProjection(cond, getValue(cond.in(1)) != 0 ? 0 : 1); + case ReturnNode ret -> { + return new Result(ResultType.VALUE, getValue(ret.in(1))); + } + case ProjNode ignored -> next = findControl(control); + default -> throw Utils.TODO("Unexpected control node " + control); + } + prev = control; + control = next; + } + return new Result(ResultType.FALLTHROUGH, 0); + } + + public static long evaluate(Node graph) { + return evaluate(graph, 0); + } + + public static long evaluate(Node graph, long parameter) { + return evaluate(graph, parameter, 1000); + } + + public static long evaluate(Node graph, long parameter, int loops) { + var res = evaluateWithResult(graph, parameter, loops); + if (res.type== ResultType.TIMEOUT) throw new RuntimeException("Timeout"); + return res.value; + } + + public static Result evaluateWithResult(Node graph, long parameter, int loops) { + var start = findStart(new BitSet(), graph); + if (start == null) return new Result(ResultType.TIMEOUT, 0); + var evaluator = new GraphEvaluator(); + return evaluator.evaluate(start, parameter, loops); + } + +} diff --git a/chapter09/src/main/java/com/seaofnodes/simple/GraphVisualizer.java b/chapter09/src/main/java/com/seaofnodes/simple/GraphVisualizer.java index 657593d2..b4d64d4c 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/GraphVisualizer.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/GraphVisualizer.java @@ -31,7 +31,7 @@ public class GraphVisualizer { sb.append("/*\n"); sb.append(stop._src); sb.append("\n*/\n"); - + // To keep the Scopes below the graph and pointing up into the graph we // need to group the Nodes in a subgraph cluster, and the scopes into a // different subgraph cluster. THEN we can draw edges between the @@ -51,7 +51,7 @@ public class GraphVisualizer { // Just the Nodes first, in a cluster no edges nodes(sb, all); - + // Now the scopes, in a cluster no edges if( xScopes != null ) for( ScopeNode xscope : xScopes ) @@ -66,7 +66,7 @@ public class GraphVisualizer { for( ScopeNode xscope : xScopes ) if( !xscope.isDead() ) scopeEdges( sb, xscope ); - + sb.append("}\n"); return sb.toString(); } @@ -200,13 +200,13 @@ public class GraphVisualizer { sb.append(" color=red"); // Backedges do not add a ranking constraint if( i==2 && (n instanceof PhiNode || n instanceof LoopNode) ) - sb.append(" constraint=false"); + sb.append(" constraint=false"); sb.append("];\n"); } } } } - + // Walk the scope edges private void scopeEdges( StringBuilder sb, ScopeNode scope ) { sb.append("\tedge [style=dashed color=cornflowerblue];\n"); @@ -233,7 +233,7 @@ public class GraphVisualizer { level++; } } - + /** * Finds all nodes in the graph. */ diff --git a/chapter09/src/main/java/com/seaofnodes/simple/IterPeeps.java b/chapter09/src/main/java/com/seaofnodes/simple/IterPeeps.java index 8103103c..6dafd95f 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/IterPeeps.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/IterPeeps.java @@ -99,7 +99,7 @@ public abstract class IterPeeps { // Visit ALL nodes and confirm the invariant: // Either you are on the WORK worklist OR running `iter()` makes no progress. - + // This invariant ensures that no progress is missed, i.e., when the // worklist is empty we have indeed done all that can be done. To help // with debugging, the {@code assert} is broken out in a place where it is easy to diff --git a/chapter09/src/main/java/com/seaofnodes/simple/Parser.java b/chapter09/src/main/java/com/seaofnodes/simple/Parser.java index 5e7da8f8..b3aeeb22 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/Parser.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/Parser.java @@ -13,7 +13,7 @@ import java.util.*; * This is a simple recursive descent parser. All lexical analysis is done here as well. */ public class Parser { - + /** * A Global Static, unique to each compilation. This is a public, so we * can make constants everywhere without having to thread the StartNode @@ -57,7 +57,7 @@ public class Parser { add("while"); }}; - + /** * We clone ScopeNodes when control flows branch; it is useful to have * a list of all active ScopeNodes for purposes of visualization of the SoN graph @@ -83,12 +83,12 @@ public class Parser { @Override public String toString() { return _lexer.toString(); } - + String src() { return new String( _lexer._input ); } // Debugging utility to find a Node by index public static Node find(int nid) { return START.find(nid); } - + private Node ctrl() { return _scope.ctrl(); } private Node ctrl(Node n) { return _scope.ctrl(n); } @@ -198,7 +198,7 @@ public class Parser { // No continues yet _continueScope = null; - + // Parse the true side, which corresponds to loop body // Our current scope is the body Scope ctrl(ifT.unkeep()); // set ctrl token to ifTrue projection @@ -281,7 +281,7 @@ public class Parser { ctrl(ifT.unkeep()); // set ctrl token to ifTrue projection parseStatement(); // Parse true-side ScopeNode tScope = _scope; - + // Parse the false side _scope = fScope; // Restore scope, then parse else block if any ctrl(ifF.unkeep()); // Ctrl token is now set to ifFalse projection @@ -292,7 +292,7 @@ public class Parser { if( tScope.nIns() != ndefs || fScope.nIns() != ndefs ) throw error("Cannot define a new name on one arm of an if"); - + // Merge results _scope = tScope; _xScopes.pop(); // Discard pushed from graph display @@ -604,7 +604,7 @@ public class Parser { skipWhiteSpace(); return peek()==ch; } - + // Return an identifier or null String matchId() { skipWhiteSpace(); @@ -631,7 +631,7 @@ public class Parser { return TypeInteger.constant(Long.parseLong(snum)); } - // First letter of an identifier + // First letter of an identifier private boolean isIdStart(char ch) { return Character.isAlphabetic(ch) || ch == '_'; } @@ -647,7 +647,7 @@ public class Parser { return new String(_input, start, --_position - start); } - // + // private boolean isPunctuation(char ch) { return "=;[]<>()+-/*".indexOf(ch) != -1; } diff --git a/chapter09/src/main/java/com/seaofnodes/simple/Utils.java b/chapter09/src/main/java/com/seaofnodes/simple/Utils.java index 4d663c25..45f15327 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/Utils.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/Utils.java @@ -36,4 +36,4 @@ public class Utils { return i; return -1; } -}
\ No newline at end of file +} diff --git a/chapter09/src/main/java/com/seaofnodes/simple/node/AddNode.java b/chapter09/src/main/java/com/seaofnodes/simple/node/AddNode.java index 5ffb821e..4e30bb55 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/node/AddNode.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/node/AddNode.java @@ -8,7 +8,7 @@ public class AddNode extends Node { public AddNode(Node lhs, Node rhs) { super(null, lhs, rhs); } @Override public String label() { return "Add"; } - + @Override public String glabel() { return "+"; } @Override @@ -17,7 +17,7 @@ public class AddNode extends Node { in(2)._print0(sb.append("+"), visited); return sb.append(")"); } - + @Override public Type compute() { @@ -39,13 +39,13 @@ public class AddNode extends Node { // canonicalize to (x+0) if( t2 instanceof TypeInteger i && i.value()==0 ) return lhs; - + // Add of same to a multiply by 2 if( lhs==rhs ) return new MulNode(lhs,new ConstantNode(TypeInteger.constant(2)).peephole()); // Goal: a left-spine set of adds, with constants on the rhs (which then fold). - + // Move non-adds to RHS if( !(lhs instanceof AddNode) && rhs instanceof AddNode ) return swap12(); @@ -53,7 +53,7 @@ public class AddNode extends Node { // x+(-y) becomes x-y if( rhs instanceof MinusNode minus ) return new SubNode(lhs,minus.in(1)); - + // Now we might see (add add non) or (add non non) or (add add add) but never (add non add) // Do we have x + (y + z) ? @@ -73,7 +73,7 @@ public class AddNode extends Node { // the loop will peep as dead after a bit. if( lhs.in(1) == lhs ) return null; - + // Do we have (x + con1) + con2? // Replace with (x + (con1+con2) which then fold the constants // lhs.in(2) is con1 here @@ -91,12 +91,12 @@ public class AddNode extends Node { if( phicon!=null ) return phicon; // Now we sort along the spine via rotates, to gather similar things together. - + // Do we rotate (x + y) + z // into (x + z) + y ? if( spine_cmp(lhs.in(2),rhs,this) ) return new AddNode(new AddNode(lhs.in(1),rhs).peephole(),lhs.in(2)); - + return null; } @@ -114,11 +114,11 @@ public class AddNode extends Node { lphi = pcon(lhs.in(2),op); // Will rotate with the Phi push } if( lphi==null ) return null; - + // RHS is a constant or a Phi of constants if( !(rhs instanceof ConstantNode) && pcon(rhs,op)==null ) return null; - + // If both are Phis, must be same Region if( rhs instanceof PhiNode && lphi.in(0) != rhs.in(0) ) return null; @@ -147,7 +147,7 @@ public class AddNode extends Node { static PhiNode pcon(Node op, Node dep) { return op instanceof PhiNode phi && phi.allCons(dep) ? phi : null; } - + // Compare two off-spine nodes and decide what order they should be in. // Do we rotate ((x + hi) + lo) into ((x + lo) + hi) ? // Generally constants always go right, then Phi-of-constants, then muls, then others. @@ -165,10 +165,10 @@ public class AddNode extends Node { if( lo instanceof PhiNode && !(hi instanceof PhiNode) ) return true; if( hi instanceof PhiNode && !(lo instanceof PhiNode) ) return false; - + // Same category of "others" return lo._nid > hi._nid; } - + @Override Node copy(Node lhs, Node rhs) { return new AddNode(lhs,rhs); } } diff --git a/chapter09/src/main/java/com/seaofnodes/simple/node/BoolNode.java b/chapter09/src/main/java/com/seaofnodes/simple/node/BoolNode.java index b4691b27..d3fcec95 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/node/BoolNode.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/node/BoolNode.java @@ -12,7 +12,7 @@ abstract public class BoolNode extends Node { } abstract String op(); // String opcode name - + @Override public String label() { return getClass().getSimpleName(); } @@ -40,7 +40,7 @@ abstract public class BoolNode extends Node { @Override public Node idealize() { - // Compare of same + // Compare of same if( in(1)==in(2) ) return new ConstantNode(TypeInteger.constant(doOp(3,3)?1:0)); diff --git a/chapter09/src/main/java/com/seaofnodes/simple/node/ConstantNode.java b/chapter09/src/main/java/com/seaofnodes/simple/node/ConstantNode.java index c4ea08b5..9e45198c 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/node/ConstantNode.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/node/ConstantNode.java @@ -23,10 +23,10 @@ public class ConstantNode extends Node { super(Parser.START); _con = type; } - + @Override public String label() { return "#"+_con; } - + @Override public String uniqueName() { return "Con_" + _nid; } @@ -34,9 +34,9 @@ public class ConstantNode extends Node { StringBuilder _print1(StringBuilder sb, BitSet visited) { return _con._print(sb); } - + @Override public boolean isMultiTail() { return true; } - + @Override public Type compute() { return _con; } diff --git a/chapter09/src/main/java/com/seaofnodes/simple/node/DivNode.java b/chapter09/src/main/java/com/seaofnodes/simple/node/DivNode.java index ed1a233c..ba06d037 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/node/DivNode.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/node/DivNode.java @@ -18,7 +18,7 @@ public class DivNode extends Node { in(2)._print0(sb.append("/"), visited); return sb.append(")"); } - + @Override public Type compute() { if (in(1)._type instanceof TypeInteger i0 && @@ -33,6 +33,6 @@ public class DivNode extends Node { @Override public Node idealize() { return null; } - + @Override Node copy(Node lhs, Node rhs) { return new DivNode(lhs,rhs); } } diff --git a/chapter09/src/main/java/com/seaofnodes/simple/node/IfNode.java b/chapter09/src/main/java/com/seaofnodes/simple/node/IfNode.java index 52486772..460dce51 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/node/IfNode.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/node/IfNode.java @@ -42,10 +42,10 @@ public class IfNode extends MultiNode { // Else true branch is reachable if (t instanceof TypeInteger ti && ti.isConstant()) return ti==TypeInteger.ZERO ? TypeTuple.IF_FALSE : TypeTuple.IF_TRUE; - + return TypeTuple.IF_BOTH; } - + @Override public Node idealize() { // Hunt up the immediate dominator tree. If we find an identical if diff --git a/chapter09/src/main/java/com/seaofnodes/simple/node/LoopNode.java b/chapter09/src/main/java/com/seaofnodes/simple/node/LoopNode.java index 0b84f897..c192bc20 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/node/LoopNode.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/node/LoopNode.java @@ -25,5 +25,5 @@ public class LoopNode extends RegionNode { return inProgress() ? null : super.idealize(); } - @Override Node idom() { return entry(); } + @Override Node idom() { return entry(); } } diff --git a/chapter09/src/main/java/com/seaofnodes/simple/node/MinusNode.java b/chapter09/src/main/java/com/seaofnodes/simple/node/MinusNode.java index 524f63a0..dd092c9e 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/node/MinusNode.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/node/MinusNode.java @@ -9,15 +9,15 @@ public class MinusNode extends Node { public MinusNode(Node in) { super(null, in); } @Override public String label() { return "Minus"; } - + @Override public String glabel() { return "-"; } - + @Override StringBuilder _print1(StringBuilder sb, BitSet visited) { in(1)._print0(sb.append("(-"), visited); return sb.append(")"); } - + @Override public Type compute() { if (in(1)._type instanceof TypeInteger i0) diff --git a/chapter09/src/main/java/com/seaofnodes/simple/node/MulNode.java b/chapter09/src/main/java/com/seaofnodes/simple/node/MulNode.java index d63d2cb1..7e9f88a2 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/node/MulNode.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/node/MulNode.java @@ -9,7 +9,7 @@ public class MulNode extends Node { public MulNode(Node lhs, Node rhs) { super(null, lhs, rhs); } @Override public String label() { return "Mul"; } - + @Override public String glabel() { return "*"; } @Override @@ -35,12 +35,12 @@ public class MulNode extends Node { Node rhs = in(2); Type t1 = lhs._type; Type t2 = rhs._type; - + // Mul of 1. We do not check for (1*x) because this will already // canonicalize to (x*1) if ( t2.isConstant() && t2 instanceof TypeInteger i && i.value()==1 ) return lhs; - + // Move constants to RHS: con*arg becomes arg*con if ( t1.isConstant() && !t2.isConstant() ) return swap12(); diff --git a/chapter09/src/main/java/com/seaofnodes/simple/node/MultiNode.java b/chapter09/src/main/java/com/seaofnodes/simple/node/MultiNode.java index eced9a14..2743bd19 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/node/MultiNode.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/node/MultiNode.java @@ -1,7 +1,7 @@ package com.seaofnodes.simple.node; public abstract class MultiNode extends Node { - + public MultiNode(Node... inputs) { super(inputs); } // Find a projection by index diff --git a/chapter09/src/main/java/com/seaofnodes/simple/node/Node.java b/chapter09/src/main/java/com/seaofnodes/simple/node/Node.java index 7c82ffd5..5eb9c076 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/node/Node.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/node/Node.java @@ -49,7 +49,7 @@ public abstract class Node { */ public Type _type; - + /** * Immediate dominator tree depth, used to approximate a real IDOM during * parsing where we do not have the whole program, and also peepholes @@ -58,7 +58,7 @@ public abstract class Node { * See {@link <a href="https://en.wikipedia.org/wiki/Dominator_(graph_theory)">...</a>} */ int _idepth; - + /** * A private Global Static mutable counter, for unique node id generation. * To make the compiler multithreaded, this field will have to move into a TLS. @@ -88,9 +88,9 @@ public abstract class Node { // ------------------------------------------------------------------------ - + // Debugger Printing. - + // {@code toString} is what you get in the debugger. It has to print 1 // line (because this is what a debugger typically displays by default) and // has to be robust with broken graph/nodes. @@ -104,7 +104,7 @@ public abstract class Node { public final String print() { return _print0(new StringBuilder(), new BitSet()).toString(); } - + // This is the common print: check for repeats, check for DEAD and print // "DEAD" else call the per-Node print1. final StringBuilder _print0(StringBuilder sb, BitSet visited) { @@ -144,13 +144,13 @@ public abstract class Node { } public String p(int depth) { return IRPrinter.prettyPrint(this,depth); } - + public boolean isMultiHead() { return false; } public boolean isMultiTail() { return false; } - + // ------------------------------------------------------------------------ // Graph Node & Edge manipulation - + /** * Gets the ith input node * @param i Offset of the input node @@ -166,19 +166,19 @@ public abstract class Node { public boolean isUnused() { return nOuts() == 0; } public boolean isCFG() { return false; } - + /** * Change a <em>def</em> into a Node. Keeps the edges correct, by removing * the corresponding <em>use->def</em> edge. This may make the original * <em>def</em> go dead. This function is co-recursive with {@link #kill}. * <p> - + * This method is the normal path for altering a Node, because it does the * proper default edge maintenance. It also <em>immediately</em> kills * Nodes that lose their last use; at times care must be taken to avoid * killing Nodes that are being used without having an output Node. This * definitely happens in the middle of recursive {@link #peephole} calls. - * + * * @param idx which def to set * @param new_def the new definition * @return new_def for flow coding @@ -200,7 +200,7 @@ public abstract class Node { // Return self for easy flow-coding return new_def; } - + // Remove the numbered input, compressing the inputs in-place. This // shuffles the order deterministically - which is suitable for Region and // Phi, but not for every Node. If the def goes dead, it is recursively @@ -255,7 +255,7 @@ public abstract class Node { old_def.kill(); // Kill old def } } - + /** * Kill a Node with no <em>uses</em>, by setting all of its <em>defs</em> * to null. This may recursively kill more Nodes, and is basically dead @@ -298,10 +298,10 @@ public abstract class Node { } kill(); } - + // ------------------------------------------------------------------------ // Graph-based optimizations - + /** * We allow disabling peephole opt so that we can observe the * full graph, vs the optimized graph. @@ -390,7 +390,7 @@ public abstract class Node { } return m; } - + /** * This function needs to be * <a href="https://en.wikipedia.org/wiki/Monotonic_function">Monotonic</a> @@ -421,7 +421,7 @@ public abstract class Node { return old; } - + /** * This function rewrites the current Node into a more "idealized" form. * This is the bulk of our peephole rewrite rules, and we use this to @@ -500,8 +500,8 @@ public abstract class Node { IterPeeps.addAll(_deps); _deps.clear(); } - - + + // Global Value Numbering. Hash over opcode and inputs; hits in this table // are structurally equal. public static final HashMap<Node,Node> GVN = new HashMap<>(); @@ -539,7 +539,7 @@ public abstract class Node { _hash=0; // Out of table now } - + // Hash of opcode and inputs @Override public final int hashCode() { if( _hash != 0 ) return _hash; @@ -552,11 +552,11 @@ public abstract class Node { } // Subclasses add extra hash info (such as ConstantNodes constant) int hash() { return 0; } - - + + // ------------------------------------------------------------------------ // Peephole utilities - + // Swap inputs without letting either input go dead during the swap. Node swap12() { unlock(); // Hash is order dependent @@ -619,7 +619,7 @@ public abstract class Node { WVISIT.clear(); return rez; } - + private <E> E _walk( Function<Node,E> pred ) { if( WVISIT.get(_nid) ) return null; // Been there, done that WVISIT.set(_nid); @@ -629,7 +629,7 @@ public abstract class Node { for( Node use : _outputs ) if( use != null && (x = use._walk(pred)) != null ) return x; return null; } - + /** * Debugging utility to find a Node by index */ diff --git a/chapter09/src/main/java/com/seaofnodes/simple/node/NotNode.java b/chapter09/src/main/java/com/seaofnodes/simple/node/NotNode.java index ab0f4d01..7a520fec 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/node/NotNode.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/node/NotNode.java @@ -9,15 +9,15 @@ public class NotNode extends Node { public NotNode(Node in) { super(null, in); } @Override public String label() { return "Not"; } - + @Override public String glabel() { return "!"; } - + @Override StringBuilder _print1(StringBuilder sb, BitSet visited) { in(1)._print0(sb.append("(!"), visited); return sb.append(")"); } - + @Override public Type compute() { if( in(1)._type instanceof TypeInteger i0 ) diff --git a/chapter09/src/main/java/com/seaofnodes/simple/node/PhiNode.java b/chapter09/src/main/java/com/seaofnodes/simple/node/PhiNode.java index 2d03b655..ec4c6ad3 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/node/PhiNode.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/node/PhiNode.java @@ -7,7 +7,7 @@ import java.util.BitSet; public class PhiNode extends Node { final String _label; - + public PhiNode(String label, Node... inputs) { super(inputs); _label = label; } @Override public String label() { return "Phi_"+_label; } @@ -59,7 +59,7 @@ public class PhiNode extends Node { return live; // Pull "down" a common data op. One less op in the world. One more - // Phi, but Phis do not make code. + // Phi, but Phis do not make code. // Phi(op(A,B),op(Q,R),op(X,Y)) becomes // op(Phi(A,Q,X), Phi(B,R,Y)). Node op = in(1); diff --git a/chapter09/src/main/java/com/seaofnodes/simple/node/ProjNode.java b/chapter09/src/main/java/com/seaofnodes/simple/node/ProjNode.java index 115bc1eb..25f595a5 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/node/ProjNode.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/node/ProjNode.java @@ -12,7 +12,7 @@ public class ProjNode extends Node { // Debugging label public final String _label; - + public ProjNode(MultiNode ctrl, int idx, String label) { super(ctrl); _idx = idx; diff --git a/chapter09/src/main/java/com/seaofnodes/simple/node/RegionNode.java b/chapter09/src/main/java/com/seaofnodes/simple/node/RegionNode.java index 9468c395..c949c2b2 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/node/RegionNode.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/node/RegionNode.java @@ -11,7 +11,7 @@ import java.util.BitSet; public class RegionNode extends Node { public RegionNode(Node... nodes) { super(nodes); } - + @Override public String label() { return "Region"; } @@ -78,7 +78,7 @@ public class RegionNode extends Node { return true; return false; } - + // Immediate dominator of Region is a little more complicated. private Node _idom; // Immediate dominator cache @Override Node idom() { diff --git a/chapter09/src/main/java/com/seaofnodes/simple/node/ReturnNode.java b/chapter09/src/main/java/com/seaofnodes/simple/node/ReturnNode.java index c10d82d8..922a3649 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/node/ReturnNode.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/node/ReturnNode.java @@ -21,7 +21,7 @@ public class ReturnNode extends Node { public Node ctrl() { return in(0); } public Node expr() { return in(1); } - + @Override public String label() { return "Return"; } @@ -31,7 +31,7 @@ public class ReturnNode extends Node { } @Override public boolean isCFG() { return true; } - + @Override public Type compute() { return TypeTuple.make(ctrl()._type,expr()._type); diff --git a/chapter09/src/main/java/com/seaofnodes/simple/node/ScopeNode.java b/chapter09/src/main/java/com/seaofnodes/simple/node/ScopeNode.java index 6f72dea2..3abea54d 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/node/ScopeNode.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/node/ScopeNode.java @@ -29,8 +29,8 @@ public class ScopeNode extends Node { _scopes = new Stack<>(); _type = Type.BOTTOM; } - - + + @Override public String label() { return "Scope"; } @Override @@ -64,7 +64,7 @@ public class ScopeNode extends Node { names[syms.get(name)] = name; return names; } - + @Override public Type compute() { return Type.BOTTOM; } @Override public Node idealize() { return null; } @@ -191,7 +191,7 @@ public class ScopeNode extends Node { that.kill(); // Kill merged scope return r.unkeep().peephole(); } - + // Merge the backedge scope into this loop head scope // We set the second input to the phi from the back edge (i.e. loop body) public void endLoop(ScopeNode back, ScopeNode exit ) { diff --git a/chapter09/src/main/java/com/seaofnodes/simple/node/StartNode.java b/chapter09/src/main/java/com/seaofnodes/simple/node/StartNode.java index 5693f27b..e399138b 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/node/StartNode.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/node/StartNode.java @@ -15,7 +15,7 @@ import java.util.BitSet; public class StartNode extends MultiNode { final TypeTuple _args; - + public StartNode(Type[] args) { super(); _args = TypeTuple.make(args); diff --git a/chapter09/src/main/java/com/seaofnodes/simple/node/StopNode.java b/chapter09/src/main/java/com/seaofnodes/simple/node/StopNode.java index dccc90a9..602feffc 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/node/StopNode.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/node/StopNode.java @@ -8,7 +8,7 @@ import java.util.BitSet; public class StopNode extends Node { public final String _src; - + public StopNode(String src) { super(); _src = src; @@ -35,7 +35,7 @@ public class StopNode extends Node { public ReturnNode ret() { return nIns()==1 ? (ReturnNode)in(0) : null; } - + @Override public Type compute() { return Type.BOTTOM; diff --git a/chapter09/src/main/java/com/seaofnodes/simple/node/SubNode.java b/chapter09/src/main/java/com/seaofnodes/simple/node/SubNode.java index f81ae6d0..ce83122c 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/node/SubNode.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/node/SubNode.java @@ -18,7 +18,7 @@ public class SubNode extends Node { in(2)._print0(sb.append("-"), visited); return sb.append(")"); } - + @Override public Type compute() { if (in(1)._type instanceof TypeInteger i0 && diff --git a/chapter09/src/main/java/com/seaofnodes/simple/type/Type.java b/chapter09/src/main/java/com/seaofnodes/simple/type/Type.java index ee6fb741..d0265e83 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/type/Type.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/type/Type.java @@ -7,7 +7,7 @@ import java.util.HashMap; /** * These types are part of a Monotone Analysis Framework, * @see <a href="https://www.cse.psu.edu/~gxt29/teaching/cse597s21/slides/08monotoneFramework.pdf">see for example this set of slides</a>. - * <p> + * <p> * The types form a lattice; @see <a href="https://en.wikipedia.org/wiki/Lattice_(order)">a symmetric complete bounded (ranked) lattice.</a> * <p> * This wild lattice theory will be needed later to allow us to easily beef up @@ -18,7 +18,7 @@ import java.util.HashMap; * actual implementation is darn near trivial and is generally really obvious * what we're doing with it. Right now, it's just simple integer math to do * simple constant folding e.g. 1+2 == 3 stuff. - */ + */ public class Type { static final HashMap<Type,Type> INTERN = new HashMap<>(); @@ -59,11 +59,11 @@ public class Type { // Factory method which interns "this" protected <T extends Type> T intern() { T nnn = (T)INTERN.get(this); - if( nnn==null ) + if( nnn==null ) INTERN.put(nnn=(T)this,this); return nnn; } - + private int _hash; // Hash cache; not-zero when set. @Override public final int hashCode() { @@ -74,7 +74,7 @@ public class Type { } // Override in subclasses int hash() { return _type; } - + @Override public final boolean equals( Object o ) { if( o==this ) return true; @@ -84,8 +84,8 @@ public class Type { } // Overridden in subclasses; subclass can assume "this!=t" and java classes are same boolean eq(Type t) { return true; } - - + + // ---------------------------------------------------------- public final Type meet(Type t) { // Shortcut for the self case @@ -114,7 +114,7 @@ public class Type { // True if this "isa" t; e.g. 17 isa TypeInteger.BOT public boolean isa( Type t ) { return meet(t)==t; } - + // ---------------------------------------------------------- // Our lattice is defined with a MEET and a DUAL. // JOIN is dual of meet of both duals. @@ -132,7 +132,7 @@ public class Type { default -> throw Utils.TODO(); // Should not reach here }; } - + // ---------------------------------------------------------- @Override public final String toString() { diff --git a/chapter09/src/main/java/com/seaofnodes/simple/type/TypeInteger.java b/chapter09/src/main/java/com/seaofnodes/simple/type/TypeInteger.java index df41a526..ba045603 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/type/TypeInteger.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/type/TypeInteger.java @@ -28,7 +28,7 @@ public class TypeInteger extends Type { public static TypeInteger constant(long con) { return make(true, con); } - @Override + @Override public StringBuilder _print(StringBuilder sb) { if( this==TOP ) return sb.append("IntTop"); if( this==BOT ) return sb.append("IntBot"); @@ -63,7 +63,7 @@ public class TypeInteger extends Type { if( isConstant() ) return this; // Constants are a self-dual return _con==0 ? BOT : TOP; } - + @Override int hash() { return (int)(_con ^ (_is_con ? 0 : 0x4000)); } @Override diff --git a/chapter09/src/main/java/com/seaofnodes/simple/type/TypeTuple.java b/chapter09/src/main/java/com/seaofnodes/simple/type/TypeTuple.java index 863c5b7d..e82ea00b 100644 --- a/chapter09/src/main/java/com/seaofnodes/simple/type/TypeTuple.java +++ b/chapter09/src/main/java/com/seaofnodes/simple/type/TypeTuple.java @@ -1,7 +1,7 @@ package com.seaofnodes.simple.type; public class TypeTuple extends Type { - + public final Type[] _types; private TypeTuple(Type[] types) { super(TTUPLE); _types = types; } @@ -23,7 +23,7 @@ public class TypeTuple extends Type { ts[i] = _types[i].dual(); return make(ts); } - + @Override public StringBuilder _print(StringBuilder sb) { sb.append("["); @@ -45,7 +45,7 @@ public class TypeTuple extends Type { for( Type type : _types ) sum ^= type.hashCode(); return sum; } - + @Override boolean eq( Type t ) { TypeTuple tt = (TypeTuple)t; // Contract diff --git a/chapter09/src/test/java/com/seaofnodes/simple/Chapter01Test.java b/chapter09/src/test/java/com/seaofnodes/simple/Chapter01Test.java index 48cc716f..5abfc4cf 100644 --- a/chapter09/src/test/java/com/seaofnodes/simple/Chapter01Test.java +++ b/chapter09/src/test/java/com/seaofnodes/simple/Chapter01Test.java @@ -16,7 +16,7 @@ public class Chapter01Test { StopNode stop = parser.parse(); StartNode start = Parser.START; ReturnNode ret = (ReturnNode)stop.in(0); - + assertTrue(ret.ctrl() instanceof ProjNode); Node expr = ret.expr(); if( expr instanceof ConstantNode con ) { @@ -40,7 +40,7 @@ public class Chapter01Test { @Test public void testBad2() { - try { + try { new Parser("return 0123;").parse(); fail(); } catch( RuntimeException e ) { diff --git a/chapter09/src/test/java/com/seaofnodes/simple/Chapter02Test.java b/chapter09/src/test/java/com/seaofnodes/simple/Chapter02Test.java index 5efd33e7..3dd22e39 100644 --- a/chapter09/src/test/java/com/seaofnodes/simple/Chapter02Test.java +++ b/chapter09/src/test/java/com/seaofnodes/simple/Chapter02Test.java @@ -9,7 +9,7 @@ import org.junit.Ignore; import static org.junit.Assert.*; public class Chapter02Test { - + @Test public void testParseGrammar() { Parser parser = new Parser("return 1+2*3+-5;"); @@ -64,10 +64,10 @@ public class Chapter02Test { GraphVisualizer gv = new GraphVisualizer(); System.out.println(gv.generateDotOutput(parser)); } - + @Test public void testBad1() { - try { + try { new Parser("ret").parse(); fail(); } catch( RuntimeException e ) { @@ -82,7 +82,7 @@ public class Chapter02Test { @Test public void testBad4() { - try { + try { new Parser("return 100").parse(); fail(); } catch( RuntimeException e ) { diff --git a/chapter09/src/test/java/com/seaofnodes/simple/Chapter03Test.java b/chapter09/src/test/java/com/seaofnodes/simple/Chapter03Test.java index 8a11107d..6bcfcc4b 100644 --- a/chapter09/src/test/java/com/seaofnodes/simple/Chapter03Test.java +++ b/chapter09/src/test/java/com/seaofnodes/simple/Chapter03Test.java @@ -9,7 +9,7 @@ import org.junit.Ignore; import static org.junit.Assert.*; public class Chapter03Test { - + @Test public void testVarDecl() { Parser parser = new Parser("int a=1; return a;"); @@ -49,7 +49,7 @@ public class Chapter03Test { @Test public void testSelfAssign() { - try { + try { new Parser("int a=a; return a;").parse(); fail(); } catch( RuntimeException e ) { diff --git a/chapter09/src/test/java/com/seaofnodes/simple/Chapter04Test.java b/chapter09/src/test/java/com/seaofnodes/simple/Chapter04Test.java index ffbbe9a8..5a024e77 100644 --- a/chapter09/src/test/java/com/seaofnodes/simple/Chapter04Test.java +++ b/chapter09/src/test/java/com/seaofnodes/simple/Chapter04Test.java @@ -9,7 +9,7 @@ import org.junit.Ignore; import static org.junit.Assert.*; public class Chapter04Test { - + @Test public void testPeephole() { Parser parser = new Parser("return 1+arg+2; #showGraph;"); @@ -37,7 +37,7 @@ public class Chapter04Test { StopNode ret = parser.parse(); assertEquals("return (arg*2);", ret.print()); } - + @Test public void testPeephole3() { Parser parser = new Parser("return 1+arg+2+arg+3; #showGraph;"); @@ -51,7 +51,7 @@ public class Chapter04Test { StopNode ret = parser.parse(); assertEquals("return arg;", ret.print()); } - + @Test public void testVarArg() { Parser parser = new Parser("return arg; #showGraph;"); @@ -112,7 +112,7 @@ public class Chapter04Test { @Test public void testBug3() { - try { + try { new Parser("inta=1; return a;").parse(); fail(); } catch( RuntimeException e ) { @@ -126,5 +126,5 @@ public class Chapter04Test { StopNode ret = parser.parse(); assertEquals("return (-arg);", ret.print()); } - + } diff --git a/chapter09/src/test/java/com/seaofnodes/simple/Chapter05Test.java b/chapter09/src/test/java/com/seaofnodes/simple/Chapter05Test.java index 6bdc0ef6..24ae80b8 100644 --- a/chapter09/src/test/java/com/seaofnodes/simple/Chapter05Test.java +++ b/chapter09/src/test/java/com/seaofnodes/simple/Chapter05Test.java @@ -9,7 +9,7 @@ import org.junit.Ignore; import static org.junit.Assert.*; public class Chapter05Test { - + @Test public void testIfStmt() { Parser parser = new Parser( @@ -26,7 +26,7 @@ return a;"""); StopNode ret = parser.parse().iterate(true); assertEquals("return Phi(Region17,(arg+2),(arg-3));", ret.toString()); } - + @Test public void testTest() { Parser parser = new Parser( @@ -41,20 +41,20 @@ return c;""", TypeInteger.BOT); StopNode ret = parser.parse().iterate(true); assertEquals("return Phi(Region16,4,3);", ret.toString()); } - + @Test public void testReturn2() { Parser parser = new Parser( """ -if( arg==1 ) - return 3; -else - return 4; +if( arg==1 ) + return 3; +else + return 4; #showGraph;""", TypeInteger.BOT); StopNode stop = parser.parse(); assertEquals("Stop[ return 3; return 4; ]", stop.toString()); } - + @Test public void testIfMergeB() { Parser parser = new Parser( @@ -140,20 +140,20 @@ return a;"""); StopNode stop = new Parser("return true;").parse(); assertEquals("return 1;",stop.toString()); } - + @Test public void testHalfDef() { - try { + try { new Parser("if( arg==1 ) int b=2; return b;").parse(); fail(); } catch( RuntimeException e ) { assertEquals("Cannot define a new name on one arm of an if",e.getMessage()); } } - + @Test public void testHalfDef2() { - try { + try { new Parser("if( arg==1 ) { int b=2; } else { int b=3; } return b;").parse(); fail(); } catch( RuntimeException e ) { @@ -163,38 +163,38 @@ return a;"""); @Test public void testRegress1() { - try { + try { new Parser("if(arg==2) int a=1; else int b=2; return a;").parse(); fail(); } catch( RuntimeException e ) { assertEquals("Cannot define a new name on one arm of an if",e.getMessage()); } } - + @Test public void testBadNum() { - try { + try { new Parser("return 1-;").parse(); fail(); } catch( RuntimeException e ) { assertEquals("Syntax error, expected an identifier or expression: ;",e.getMessage()); } } - + @Test public void testKeyword1() { - try { + try { new Parser("int true=0; return true;").parse(); fail(); } catch( RuntimeException e ) { assertEquals("Expected an identifier, found 'true'",e.getMessage()); } } - + @Test public void testKeyword2() { - try { + try { new Parser("int else=arg; if(else) else=2; else else=1; return else;").parse(); fail(); } catch( RuntimeException e ) { @@ -204,7 +204,7 @@ return a;"""); @Test public void testKeyword3() { - try { + try { new Parser("int a=1; ififif(arg)inta=2;return a;").parse(); fail(); } catch( RuntimeException e ) { diff --git a/chapter09/src/test/java/com/seaofnodes/simple/Chapter06Test.java b/chapter09/src/test/java/com/seaofnodes/simple/Chapter06Test.java index ba0ea9f6..5fece2e2 100644 --- a/chapter09/src/test/java/com/seaofnodes/simple/Chapter06Test.java +++ b/chapter09/src/test/java/com/seaofnodes/simple/Chapter06Test.java @@ -292,5 +292,5 @@ return a+b+c; StopNode ret = parser.parse().iterate(true); assertEquals("return 5;", ret.toString()); } - + } diff --git a/chapter09/src/test/java/com/seaofnodes/simple/Chapter07Test.java b/chapter09/src/test/java/com/seaofnodes/simple/Chapter07Test.java index 48876598..d42af6c3 100644 --- a/chapter09/src/test/java/com/seaofnodes/simple/Chapter07Test.java +++ b/chapter09/src/test/java/com/seaofnodes/simple/Chapter07Test.java @@ -10,7 +10,7 @@ import static org.junit.Assert.*; public class Chapter07Test { - + @Test public void testExample() { Parser parser = new Parser( @@ -44,7 +44,7 @@ return a; StopNode stop = parser.parse().iterate(true); assertEquals("return Phi(Region22,1,Phi(Loop11,1,(Phi_a+1)));", stop.toString()); } - + @Test public void testWhileNested() { Parser parser = new Parser( @@ -242,4 +242,3 @@ return a; assertTrue(stop.ret().ctrl() instanceof ProjNode); } } -
\ No newline at end of file diff --git a/chapter09/src/test/java/com/seaofnodes/simple/Chapter08Test.java b/chapter09/src/test/java/com/seaofnodes/simple/Chapter08Test.java index 18371494..05485d8c 100644 --- a/chapter09/src/test/java/com/seaofnodes/simple/Chapter08Test.java +++ b/chapter09/src/test/java/com/seaofnodes/simple/Chapter08Test.java @@ -145,10 +145,10 @@ return arg; StopNode stop = parser.parse().iterate(true); assertEquals("return 0;", stop.toString()); } - + @Test public void testBreakOutsideLoop() { - try { + try { new Parser(""" if(arg <= 10) { break; @@ -173,7 +173,7 @@ return arg; StopNode stop = parser.parse().iterate(true); assertEquals("return arg;", stop.toString()); } - + @Test public void testRegress4() { Parser parser = new Parser(""" @@ -189,9 +189,9 @@ return a; """); StopNode stop = parser.parse().iterate(true); assertEquals("return Phi(Region28,Phi(Loop7,1,(Phi_a+1)),Add);", stop.toString()); - assertTrue(stop.ret().ctrl() instanceof RegionNode); + assertTrue(stop.ret().ctrl() instanceof RegionNode); } - + @Test public void testRegress5() { Parser parser = new Parser(""" diff --git a/chapter09/src/test/java/com/seaofnodes/simple/Chapter09Test.java b/chapter09/src/test/java/com/seaofnodes/simple/Chapter09Test.java index 8e198218..834ba3d4 100644 --- a/chapter09/src/test/java/com/seaofnodes/simple/Chapter09Test.java +++ b/chapter09/src/test/java/com/seaofnodes/simple/Chapter09Test.java @@ -149,7 +149,7 @@ while(v1+arg) { if(0) while(0) { int arg=arg; while(0) {} -} +} """); StopNode stop = parser.parse().iterate(true); assertEquals("Stop[ ]", stop.toString()); @@ -170,7 +170,7 @@ if(0) while(0) { assertEquals("return 3;", stop.toString()); } - + @Test public void testFuzz0() { Parser parser = new Parser(""" @@ -209,7 +209,7 @@ return -0+0+0; StopNode stop = parser.parse().iterate(true); assertEquals("return 0;", stop.toString()); } - + @Test public void testFuzz3() { Parser parser = new Parser("int v0=0; while(0==69) while(v0) return 0;"); diff --git a/chapter09/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java b/chapter09/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java index 90817d2b..936fb41e 100644 --- a/chapter09/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java +++ b/chapter09/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java @@ -1,170 +1,170 @@ -package com.seaofnodes.simple.fuzzer;
-
-import com.seaofnodes.simple.GraphEvaluator;
-import com.seaofnodes.simple.Parser;
-import com.seaofnodes.simple.node.Node;
-import com.seaofnodes.simple.node.StopNode;
-
-import java.util.ArrayList;
-import java.util.Random;
-import java.util.function.Consumer;
-
-/**
- * Implementation of the fuzzer.
- * Random testcases are generated using the ScriptGenerator.
- * This will generate a script similar to how the parser parses it but instead of parsing it will
- * randomly choose a variation the parser would parse and generate it.
- * These scripts are the parsed by the parser and all exceptions are caught and filtered
- * to only show one occurrence of one problem. This includes script which generate different
- * results when the compiled graph with and without peeps is executed.
- * To aid debugging scripts that cause errors are then reduced by applying rules and checking
- * that the same issue persists.
- */
-public class Fuzzer {
-
- private static final int EVAL_TIMEOUT = 1000;
-
- /**
- * List of exceptions already encountered.
- * This is used to filter new exceptions and discard them if they are already found.
- */
- private final ArrayList<Throwable> exceptions = new ArrayList<>();
-
- /**
- * Filter and record an exception in a script.
- * @param e The exception caused by the script
- * @param script The script causing the exception
- * @param reproducer Reproducer which should be used to reproduce the test case.
- */
- private void recordException(Throwable e, String script, Consumer<String> reproducer) {
- for (var ex : exceptions) {
- if (FuzzerUtils.isExceptionFromSameCause(ex, e)) return;
- }
- exceptions.add(e);
- System.out.println("========== Stack ==========");
- e.printStackTrace(System.out);
- System.out.println("========== Code ===========");
- System.out.println(script);
- System.out.println("========= Reduced =========");
- System.out.println(Reducer.reduce(script, e, reproducer));
- System.out.println("===========================");
- System.out.flush();
- }
-
- /**
- * Check that two graphs result in the same output when supplied with a value
- * @param stop1 Stop node of the first graph
- * @param stop2 Stop node of the second graph
- * @param in The input value to both graphs to test for an equal output
- */
- private static void checkGraphs(StopNode stop1, StopNode stop2, long in) {
- var e1 = GraphEvaluator.evaluateWithResult(stop1, in, EVAL_TIMEOUT);
- var e2 = GraphEvaluator.evaluateWithResult(stop2, in, EVAL_TIMEOUT);
- if (e1.type() == GraphEvaluator.ResultType.TIMEOUT || e2.type() == GraphEvaluator.ResultType.TIMEOUT) return;
- if (e1.type() != e2.type())
- throw new RuntimeException("Different calculations types " + e1.type() + " vs " + e2.type());
- if (e1.value() != e2.value())
- throw new RuntimeException("Different calculations values " + e1.value() + " vs " + e2.value());
- }
-
- /**
- * Run checks for script. Compile the script with peeps enabled and disabled.
- * Check that exceptions raised in the parser by both methods are the same and only happens if the script may be invalid.
- * If the script was successfully parsed check that both version behave the same.
- * @param script The script to test
- * @param valid If the script is definitely valid. If not some exceptions may be suppressed.
- */
- private static void runCheck(String script, boolean valid) {
- StopNode stop1;
- try {
- stop1 = FuzzerUtils.parse(script, false);
- } catch (RuntimeException e1) {
- try {
- FuzzerUtils.parse(script, true);
- } catch (RuntimeException e2) {
- if (FuzzerUtils.isExceptionFromSameCause(e1, e2)) {
- if (!valid || e1.getClass() == RuntimeException.class) return;
- } else {
- e1.addSuppressed(e2);
- }
- }
- throw e1;
- }
- var stop2 = FuzzerUtils.parse(script, true);
- checkGraphs(stop1, stop2, 0);
- checkGraphs(stop1, stop2, 1);
- checkGraphs(stop1, stop2, 10);
- }
-
- /**
- * Check that the script does not result in exceptions.
- * @param script The script to test.
- * @param valid If the script is valid or if it might contain syntax errors.
- */
- private void check(String script, boolean valid) {
- try {
- runCheck(script, valid);
- } catch (Throwable e) {
- recordException(e, script, s->runCheck(s, valid));
- }
- }
-
- /**
- * Run one test with the given seed, and check for peephole counts.
- * @param seed The seed to use for generating this test case
- */
- public int fuzzPeepTiming(long seed, int max_nids) {
- var rand = new Random(seed);
- var sb = new StringBuilder();
- new ScriptGenerator(rand, sb, true).genProgram();
- try {
- var parser = new Parser(sb.toString());
-
- var stop = parser.parse();
- int parse_peeps= Node.ITER_CNT;
- int parse_nops = Node.ITER_NOP_CNT;
- double parse_nop_ratio = (double)parse_nops/parse_peeps;
- int nids = Node.UID();
- if( nids <= max_nids ) return max_nids;
-
- double parse_peeps_per_node = (double)parse_peeps/nids;
-
- stop.iterate();
- int iter_peeps= Node.ITER_CNT;
- int iter_nops = Node.ITER_NOP_CNT;
- double iter_nop_ratio = (double)iter_nops/iter_peeps;
- double iter_peeps_per_node = (double)iter_peeps/nids;
-
- System.out.printf("%6d | Parsing: peeps: %5d, nops: %5d, ratio: %5.3f, last UID: %5d ps/node: %5.3f | Iter: peeps: %6d, nops: %6d, ratio: %5.3f, ps/node: %5.3f\n",
- seed,
- parse_peeps,parse_nops,parse_nop_ratio,nids,parse_peeps_per_node,
- iter_peeps , iter_nops, iter_nop_ratio, iter_peeps_per_node);
- return nids;
- } catch (Throwable e) {
- // ignore exceptions, let the normal fullPeeps catch and reduce,
- // we're interested in valid program optimizations
- }
- return max_nids;
- }
-
-
- /**
- * Run one test with the given seed.
- * @param seed The seed to use for generating this test case
- */
- public void fuzzPeeps(long seed) {
- var rand = new Random(seed);
- var sb = new StringBuilder();
- var valid = new ScriptGenerator(rand, sb, false).genProgram();
- check(sb.toString(), valid);
- }
-
-
- /**
- * Check that no exceptions happened.
- * @return true if no exception happened
- */
- public boolean noExceptions() {
- return exceptions.isEmpty();
- }
-}
+package com.seaofnodes.simple.fuzzer; + +import com.seaofnodes.simple.GraphEvaluator; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.StopNode; + +import java.util.ArrayList; +import java.util.Random; +import java.util.function.Consumer; + +/** + * Implementation of the fuzzer. + * Random testcases are generated using the ScriptGenerator. + * This will generate a script similar to how the parser parses it but instead of parsing it will + * randomly choose a variation the parser would parse and generate it. + * These scripts are the parsed by the parser and all exceptions are caught and filtered + * to only show one occurrence of one problem. This includes script which generate different + * results when the compiled graph with and without peeps is executed. + * To aid debugging scripts that cause errors are then reduced by applying rules and checking + * that the same issue persists. + */ +public class Fuzzer { + + private static final int EVAL_TIMEOUT = 1000; + + /** + * List of exceptions already encountered. + * This is used to filter new exceptions and discard them if they are already found. + */ + private final ArrayList<Throwable> exceptions = new ArrayList<>(); + + /** + * Filter and record an exception in a script. + * @param e The exception caused by the script + * @param script The script causing the exception + * @param reproducer Reproducer which should be used to reproduce the test case. + */ + private void recordException(Throwable e, String script, Consumer<String> reproducer) { + for (var ex : exceptions) { + if (FuzzerUtils.isExceptionFromSameCause(ex, e)) return; + } + exceptions.add(e); + System.out.println("========== Stack =========="); + e.printStackTrace(System.out); + System.out.println("========== Code ==========="); + System.out.println(script); + System.out.println("========= Reduced ========="); + System.out.println(Reducer.reduce(script, e, reproducer)); + System.out.println("==========================="); + System.out.flush(); + } + + /** + * Check that two graphs result in the same output when supplied with a value + * @param stop1 Stop node of the first graph + * @param stop2 Stop node of the second graph + * @param in The input value to both graphs to test for an equal output + */ + private static void checkGraphs(StopNode stop1, StopNode stop2, long in) { + var e1 = GraphEvaluator.evaluateWithResult(stop1, in, EVAL_TIMEOUT); + var e2 = GraphEvaluator.evaluateWithResult(stop2, in, EVAL_TIMEOUT); + if (e1.type() == GraphEvaluator.ResultType.TIMEOUT || e2.type() == GraphEvaluator.ResultType.TIMEOUT) return; + if (e1.type() != e2.type()) + throw new RuntimeException("Different calculations types " + e1.type() + " vs " + e2.type()); + if (e1.value() != e2.value()) + throw new RuntimeException("Different calculations values " + e1.value() + " vs " + e2.value()); + } + + /** + * Run checks for script. Compile the script with peeps enabled and disabled. + * Check that exceptions raised in the parser by both methods are the same and only happens if the script may be invalid. + * If the script was successfully parsed check that both version behave the same. + * @param script The script to test + * @param valid If the script is definitely valid. If not some exceptions may be suppressed. + */ + private static void runCheck(String script, boolean valid) { + StopNode stop1; + try { + stop1 = FuzzerUtils.parse(script, false); + } catch (RuntimeException e1) { + try { + FuzzerUtils.parse(script, true); + } catch (RuntimeException e2) { + if (FuzzerUtils.isExceptionFromSameCause(e1, e2)) { + if (!valid || e1.getClass() == RuntimeException.class) return; + } else { + e1.addSuppressed(e2); + } + } + throw e1; + } + var stop2 = FuzzerUtils.parse(script, true); + checkGraphs(stop1, stop2, 0); + checkGraphs(stop1, stop2, 1); + checkGraphs(stop1, stop2, 10); + } + + /** + * Check that the script does not result in exceptions. + * @param script The script to test. + * @param valid If the script is valid or if it might contain syntax errors. + */ + private void check(String script, boolean valid) { + try { + runCheck(script, valid); + } catch (Throwable e) { + recordException(e, script, s->runCheck(s, valid)); + } + } + + /** + * Run one test with the given seed, and check for peephole counts. + * @param seed The seed to use for generating this test case + */ + public int fuzzPeepTiming(long seed, int max_nids) { + var rand = new Random(seed); + var sb = new StringBuilder(); + new ScriptGenerator(rand, sb, true).genProgram(); + try { + var parser = new Parser(sb.toString()); + + var stop = parser.parse(); + int parse_peeps= Node.ITER_CNT; + int parse_nops = Node.ITER_NOP_CNT; + double parse_nop_ratio = (double)parse_nops/parse_peeps; + int nids = Node.UID(); + if( nids <= max_nids ) return max_nids; + + double parse_peeps_per_node = (double)parse_peeps/nids; + + stop.iterate(); + int iter_peeps= Node.ITER_CNT; + int iter_nops = Node.ITER_NOP_CNT; + double iter_nop_ratio = (double)iter_nops/iter_peeps; + double iter_peeps_per_node = (double)iter_peeps/nids; + + System.out.printf("%6d | Parsing: peeps: %5d, nops: %5d, ratio: %5.3f, last UID: %5d ps/node: %5.3f | Iter: peeps: %6d, nops: %6d, ratio: %5.3f, ps/node: %5.3f\n", + seed, + parse_peeps,parse_nops,parse_nop_ratio,nids,parse_peeps_per_node, + iter_peeps , iter_nops, iter_nop_ratio, iter_peeps_per_node); + return nids; + } catch (Throwable e) { + // ignore exceptions, let the normal fullPeeps catch and reduce, + // we're interested in valid program optimizations + } + return max_nids; + } + + + /** + * Run one test with the given seed. + * @param seed The seed to use for generating this test case + */ + public void fuzzPeeps(long seed) { + var rand = new Random(seed); + var sb = new StringBuilder(); + var valid = new ScriptGenerator(rand, sb, false).genProgram(); + check(sb.toString(), valid); + } + + + /** + * Check that no exceptions happened. + * @return true if no exception happened + */ + public boolean noExceptions() { + return exceptions.isEmpty(); + } +} diff --git a/chapter09/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java b/chapter09/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java index a331e5a0..f13a51e1 100644 --- a/chapter09/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java +++ b/chapter09/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java @@ -1,138 +1,138 @@ -package com.seaofnodes.simple.fuzzer;
-
-import com.seaofnodes.simple.IterPeeps;
-import com.seaofnodes.simple.Parser;
-import com.seaofnodes.simple.node.Node;
-import com.seaofnodes.simple.node.StopNode;
-
-import java.io.OutputStream;
-import java.io.PrintStream;
-import java.lang.invoke.MethodHandle;
-import java.lang.invoke.MethodHandles;
-import java.lang.reflect.Field;
-import java.util.BitSet;
-
-/**
- * Some utilities for the fuzzer.
- */
-class FuzzerUtils {
-
- /**
- * Get the value of a private field
- */
- @SuppressWarnings("unchecked")
- public static <T> T getFieldValue(Object obj, String name) throws NoSuchFieldException, IllegalAccessException {
- return (T) (obj instanceof Class<?> clz ? getField(clz, name).get(null) : getField(obj.getClass(), name).get(obj));
- }
-
- /**
- * Get access to a private field
- */
- public static Field getField(Class<?> clz, String name) throws NoSuchFieldException {
- var field = clz.getDeclaredField(name);
- try {
- field.setAccessible(true);
- } catch (Throwable e) {
- // Intentionally left empty
- }
- return field;
- }
-
- /**
- * Try to rethrow an exception or create a runtime exception and throw it.
- */
- public static RuntimeException rethrow(Throwable e) {
- if (e instanceof RuntimeException re) return re;
- if (e instanceof Error er) throw er;
- return new RuntimeException(e);
- }
-
- /**
- * Copy of Node.WVISIST
- * Used to clear as exceptions might happen in the walk and left this bitset not cleared
- */
- private static final BitSet NodeWalkVisit;
-
- /**
- * Write access to Iterate.MID_ASSERT
- * Exception could happen mid assert and left this at true. Used to reset to false prior to parsing.
- */
- private static final MethodHandle set_MID_ASSERT;
-
- /**
- * Some problems might trigger debug output. Suppress this output with a null print stream during fuzzing.
- */
- public static final PrintStream NULL_PRINT_STREAM = new PrintStream(OutputStream.nullOutputStream());
-
- static {
- try {
- NodeWalkVisit = getFieldValue(Node.class, "WVISIT");
- set_MID_ASSERT = MethodHandles.lookup().unreflectSetter(getField( IterPeeps.class, "MID_ASSERT"));
- } catch (Exception e) {
- throw new ExceptionInInitializerError(e);
- }
- }
-
- /**
- * Check that eiter one of the objects is null or that both are equal.
- */
- private static boolean equalOrNull(Object a, Object b) {
- if (a == null || b == null) return true;
- return a.equals(b);
- }
-
- /**
- * Check if both stack traces have the same first entry. Empty stack traces can happen with NPE optimization
- * and allow the other to be any stack trace.
- */
- private static boolean sameStackTraceOrigin(StackTraceElement[] a, StackTraceElement[] b) {
- if (a == null || a.length == 0 || b == null || b.length == 0) return true; // One or both stack traces are missing.
- return a[0].equals(b[0]);
- }
-
- /**
- * Checks if two exceptions are caused by the same problem.
- */
- public static boolean isExceptionFromSameCause(Throwable a, Throwable b) {
- if (a == null || b == null) return a == b; // Handle the null case
- if (a.getClass() != b.getClass()) return false; // Different classes
- if (!equalOrNull(a.getMessage(), b.getMessage())) return false; // Different messages. Null is allowed as wildcard
- var s1 = a.getStackTrace();
- var s2 = b.getStackTrace();
- if (!sameStackTraceOrigin(s1, s2)) return false; // Different stack trace origins
- if (!isExceptionFromSameCause(a.getCause(), b.getCause())) return false; // Different causes
- var o1 = a.getSuppressed();
- var o2 = b.getSuppressed();
- if (o1 == null || o2 == null || o1.length != o2.length) return o1 == o2; // Different suppression
- for (int i=0; i<o1.length; i++) {
- if (!isExceptionFromSameCause(o1[i], o2[i])) return false; // Different suppression
- }
- return true;
- }
-
- /**
- * Parse script with peepholes enabled or disabled
- */
- public static StopNode parse(String script, boolean runPeeps) {
- var err = System.err;
- var out = System.out;
- try {
- System.setErr(NULL_PRINT_STREAM);
- System.setOut(NULL_PRINT_STREAM);
- try {
- set_MID_ASSERT.invokeExact(false);
- } catch (Throwable e) {
- throw rethrow(e);
- }
- var parser = new Parser(script);
- Node._disablePeephole = !runPeeps;
- var stop = parser.parse();
- return runPeeps ? stop.iterate() : stop;
- } finally {
- NodeWalkVisit.clear();
- System.setErr(err);
- System.setOut(out);
- }
- }
-
-}
+package com.seaofnodes.simple.fuzzer; + +import com.seaofnodes.simple.IterPeeps; +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.node.Node; +import com.seaofnodes.simple.node.StopNode; + +import java.io.OutputStream; +import java.io.PrintStream; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.util.BitSet; + +/** + * Some utilities for the fuzzer. + */ +class FuzzerUtils { + + /** + * Get the value of a private field + */ + @SuppressWarnings("unchecked") + public static <T> T getFieldValue(Object obj, String name) throws NoSuchFieldException, IllegalAccessException { + return (T) (obj instanceof Class<?> clz ? getField(clz, name).get(null) : getField(obj.getClass(), name).get(obj)); + } + + /** + * Get access to a private field + */ + public static Field getField(Class<?> clz, String name) throws NoSuchFieldException { + var field = clz.getDeclaredField(name); + try { + field.setAccessible(true); + } catch (Throwable e) { + // Intentionally left empty + } + return field; + } + + /** + * Try to rethrow an exception or create a runtime exception and throw it. + */ + public static RuntimeException rethrow(Throwable e) { + if (e instanceof RuntimeException re) return re; + if (e instanceof Error er) throw er; + return new RuntimeException(e); + } + + /** + * Copy of Node.WVISIST + * Used to clear as exceptions might happen in the walk and left this bitset not cleared + */ + private static final BitSet NodeWalkVisit; + + /** + * Write access to Iterate.MID_ASSERT + * Exception could happen mid assert and left this at true. Used to reset to false prior to parsing. + */ + private static final MethodHandle set_MID_ASSERT; + + /** + * Some problems might trigger debug output. Suppress this output with a null print stream during fuzzing. + */ + public static final PrintStream NULL_PRINT_STREAM = new PrintStream(OutputStream.nullOutputStream()); + + static { + try { + NodeWalkVisit = getFieldValue(Node.class, "WVISIT"); + set_MID_ASSERT = MethodHandles.lookup().unreflectSetter(getField( IterPeeps.class, "MID_ASSERT")); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + + /** + * Check that eiter one of the objects is null or that both are equal. + */ + private static boolean equalOrNull(Object a, Object b) { + if (a == null || b == null) return true; + return a.equals(b); + } + + /** + * Check if both stack traces have the same first entry. Empty stack traces can happen with NPE optimization + * and allow the other to be any stack trace. + */ + private static boolean sameStackTraceOrigin(StackTraceElement[] a, StackTraceElement[] b) { + if (a == null || a.length == 0 || b == null || b.length == 0) return true; // One or both stack traces are missing. + return a[0].equals(b[0]); + } + + /** + * Checks if two exceptions are caused by the same problem. + */ + public static boolean isExceptionFromSameCause(Throwable a, Throwable b) { + if (a == null || b == null) return a == b; // Handle the null case + if (a.getClass() != b.getClass()) return false; // Different classes + if (!equalOrNull(a.getMessage(), b.getMessage())) return false; // Different messages. Null is allowed as wildcard + var s1 = a.getStackTrace(); + var s2 = b.getStackTrace(); + if (!sameStackTraceOrigin(s1, s2)) return false; // Different stack trace origins + if (!isExceptionFromSameCause(a.getCause(), b.getCause())) return false; // Different causes + var o1 = a.getSuppressed(); + var o2 = b.getSuppressed(); + if (o1 == null || o2 == null || o1.length != o2.length) return o1 == o2; // Different suppression + for (int i=0; i<o1.length; i++) { + if (!isExceptionFromSameCause(o1[i], o2[i])) return false; // Different suppression + } + return true; + } + + /** + * Parse script with peepholes enabled or disabled + */ + public static StopNode parse(String script, boolean runPeeps) { + var err = System.err; + var out = System.out; + try { + System.setErr(NULL_PRINT_STREAM); + System.setOut(NULL_PRINT_STREAM); + try { + set_MID_ASSERT.invokeExact(false); + } catch (Throwable e) { + throw rethrow(e); + } + var parser = new Parser(script); + Node._disablePeephole = !runPeeps; + var stop = parser.parse(); + return runPeeps ? stop.iterate() : stop; + } finally { + NodeWalkVisit.clear(); + System.setErr(err); + System.setOut(out); + } + } + +} diff --git a/chapter09/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java b/chapter09/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java index 38a4531b..2ef0a3fd 100644 --- a/chapter09/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java +++ b/chapter09/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java @@ -1,144 +1,144 @@ -package com.seaofnodes.simple.fuzzer;
-
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-import java.util.regex.Pattern;
-
-/**
- * This class is used to reduce test cases created by the fuzzer.
- * It applies rewrite rules to the test case and checks that the
- * test still produces the same exception.
- * This is done in a fixpoint iteration until every rewrite produces
- * no or a different exception.
- */
-public class Reducer {
-
- /**
- * Holder for the rewrite rules
- */
- private record Replace(Pattern p, String r) {}
-
- /**
- * List of rewrite rules.
- */
- private static final Replace[] REPLACEMENTS = {
- new Replace(Pattern.compile("(?<=[+\\-*/<>=(])--"), ""),
- new Replace(Pattern.compile("\\b--"), "+"),
- new Replace(Pattern.compile("\\b\\+-"), "-"),
- new Replace(Pattern.compile("\\bfalse\\b"), "0"),
- new Replace(Pattern.compile("\\btrue\\b"), "1"),
- new Replace(Pattern.compile("-"), ""),
- new Replace(Pattern.compile("(?<![+\\-*/<>=])(?:-|\\*|/|>=|<=|>|<|!=|==)(?![+\\-*/<>=])"), "+"),
- new Replace(Pattern.compile("\\{(?:[^{}]|\\{})+}"), "{}"),
- new Replace(Pattern.compile("\\{((?:[^{}]|\\{})+)}"), "$1"),
- new Replace(Pattern.compile("\\(([^()]+)\\)"), "$1"),
- new Replace(Pattern.compile("\\b\\w+\\b"), "0"),
- new Replace(Pattern.compile("\\b(?:[^\\W0]|\\w\\w+)\\b"), "1"),
- new Replace(Pattern.compile("\\d+\\+"), ""),
- new Replace(Pattern.compile("\\+\\d+"), ""),
- new Replace(Pattern.compile("(?<=\\n|^)[^\\n]*(?=\\n|$)"), "\n"),
- new Replace(Pattern.compile("\\n+"), "\n"),
- new Replace(Pattern.compile("^\\n+|\\n+$"), ""),
- new Replace(Pattern.compile("else *"), ""),
- new Replace(Pattern.compile("\\bif\\([^()]+\\) *"), ""),
- new Replace(Pattern.compile("\\bwhile\\([^()]+\\) *"), ""),
- };
-
- /**
- * Predicate to test if the reduced script generates the same error.
- */
- private final Predicate<String> tester;
-
- /**
- * Create a reducer with a test function.
- * @param tester Test function which returns true when the same issue is found in the script.
- */
- private Reducer(Predicate<String> tester) {
- this.tester = tester;
- }
-
- /**
- * Rewrites the script with one rewrite rule.
- */
- private String doReplacement(String script, Pattern pattern, String with) {
- var matcher = pattern.matcher(script);
- var sb = new StringBuilder();
- int last = 0;
- while (matcher.find()) {
- var pos = sb.length();
- matcher.appendReplacement(sb, with);
- var tail = sb.length();
- matcher.appendTail(sb);
- if (tester.test(sb.toString())) {
- sb.setLength(tail);
- } else {
- sb.setLength(pos);
- sb.append(script, last, matcher.end());
- }
- last = matcher.end();
- }
- matcher.appendTail(sb);
- return sb.toString();
- }
-
- /**
- * Clean up variable names from the random ones generated by the fuzzer.
- */
- private String cleanVariables(String script) {
- var matcher = Pattern.compile("\\bint ([0-9a-zA-Z_]+)").matcher(script);
- int num = 0;
- while (matcher.find()) {
- var var = matcher.group(1);
- if (var.matches("v\\d+")) continue;
- while (script.contains("v"+num)) num++;
- var n = script.replaceAll("\\b"+var+"\\b", "v" + num);
- if (tester.test(n)) {
- script = n;
- }
- }
- return script;
- }
-
- /**
- * Run all rewrite rules once.
- */
- private String doAllReplacements(String script) {
- for (var replace: REPLACEMENTS) {
- script = doReplacement(script, replace.p, replace.r);
- }
- return script;
- }
-
- /**
- * Reduce the script to a minimal version which produces the same problem with a test function.
- * @param tester Test function which returns true when the same issue is found in the script.
- */
- public static String reduce(String script, Predicate<String> tester) {
- var reducer = new Reducer(tester);
- String old;
- do {
- old = script;
- script = reducer.doAllReplacements(script);
- script = reducer.cleanVariables(script);
- } while (!script.equals(old));
- return script;
- }
-
- /**
- * Reduce the script to a minimal version which produces the same problem with a reproducer.
- * @param ex The exception originally caused by the script
- * @param reproducer Test function which throws an exception which will be checked against ex.
- */
- public static String reduce(String script, Throwable ex, Consumer<String> reproducer) {
- return reduce(script, s->{
- Throwable nex = null;
- try {
- reproducer.accept(s);
- } catch (Throwable e) {
- nex = e;
- }
- return FuzzerUtils.isExceptionFromSameCause(nex, ex);
- });
- }
-
-}
+package com.seaofnodes.simple.fuzzer; + +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +/** + * This class is used to reduce test cases created by the fuzzer. + * It applies rewrite rules to the test case and checks that the + * test still produces the same exception. + * This is done in a fixpoint iteration until every rewrite produces + * no or a different exception. + */ +public class Reducer { + + /** + * Holder for the rewrite rules + */ + private record Replace(Pattern p, String r) {} + + /** + * List of rewrite rules. + */ + private static final Replace[] REPLACEMENTS = { + new Replace(Pattern.compile("(?<=[+\\-*/<>=(])--"), ""), + new Replace(Pattern.compile("\\b--"), "+"), + new Replace(Pattern.compile("\\b\\+-"), "-"), + new Replace(Pattern.compile("\\bfalse\\b"), "0"), + new Replace(Pattern.compile("\\btrue\\b"), "1"), + new Replace(Pattern.compile("-"), ""), + new Replace(Pattern.compile("(?<![+\\-*/<>=])(?:-|\\*|/|>=|<=|>|<|!=|==)(?![+\\-*/<>=])"), "+"), + new Replace(Pattern.compile("\\{(?:[^{}]|\\{})+}"), "{}"), + new Replace(Pattern.compile("\\{((?:[^{}]|\\{})+)}"), "$1"), + new Replace(Pattern.compile("\\(([^()]+)\\)"), "$1"), + new Replace(Pattern.compile("\\b\\w+\\b"), "0"), + new Replace(Pattern.compile("\\b(?:[^\\W0]|\\w\\w+)\\b"), "1"), + new Replace(Pattern.compile("\\d+\\+"), ""), + new Replace(Pattern.compile("\\+\\d+"), ""), + new Replace(Pattern.compile("(?<=\\n|^)[^\\n]*(?=\\n|$)"), "\n"), + new Replace(Pattern.compile("\\n+"), "\n"), + new Replace(Pattern.compile("^\\n+|\\n+$"), ""), + new Replace(Pattern.compile("else *"), ""), + new Replace(Pattern.compile("\\bif\\([^()]+\\) *"), ""), + new Replace(Pattern.compile("\\bwhile\\([^()]+\\) *"), ""), + }; + + /** + * Predicate to test if the reduced script generates the same error. + */ + private final Predicate<String> tester; + + /** + * Create a reducer with a test function. + * @param tester Test function which returns true when the same issue is found in the script. + */ + private Reducer(Predicate<String> tester) { + this.tester = tester; + } + + /** + * Rewrites the script with one rewrite rule. + */ + private String doReplacement(String script, Pattern pattern, String with) { + var matcher = pattern.matcher(script); + var sb = new StringBuilder(); + int last = 0; + while (matcher.find()) { + var pos = sb.length(); + matcher.appendReplacement(sb, with); + var tail = sb.length(); + matcher.appendTail(sb); + if (tester.test(sb.toString())) { + sb.setLength(tail); + } else { + sb.setLength(pos); + sb.append(script, last, matcher.end()); + } + last = matcher.end(); + } + matcher.appendTail(sb); + return sb.toString(); + } + + /** + * Clean up variable names from the random ones generated by the fuzzer. + */ + private String cleanVariables(String script) { + var matcher = Pattern.compile("\\bint ([0-9a-zA-Z_]+)").matcher(script); + int num = 0; + while (matcher.find()) { + var var = matcher.group(1); + if (var.matches("v\\d+")) continue; + while (script.contains("v"+num)) num++; + var n = script.replaceAll("\\b"+var+"\\b", "v" + num); + if (tester.test(n)) { + script = n; + } + } + return script; + } + + /** + * Run all rewrite rules once. + */ + private String doAllReplacements(String script) { + for (var replace: REPLACEMENTS) { + script = doReplacement(script, replace.p, replace.r); + } + return script; + } + + /** + * Reduce the script to a minimal version which produces the same problem with a test function. + * @param tester Test function which returns true when the same issue is found in the script. + */ + public static String reduce(String script, Predicate<String> tester) { + var reducer = new Reducer(tester); + String old; + do { + old = script; + script = reducer.doAllReplacements(script); + script = reducer.cleanVariables(script); + } while (!script.equals(old)); + return script; + } + + /** + * Reduce the script to a minimal version which produces the same problem with a reproducer. + * @param ex The exception originally caused by the script + * @param reproducer Test function which throws an exception which will be checked against ex. + */ + public static String reduce(String script, Throwable ex, Consumer<String> reproducer) { + return reduce(script, s->{ + Throwable nex = null; + try { + reproducer.accept(s); + } catch (Throwable e) { + nex = e; + } + return FuzzerUtils.isExceptionFromSameCause(nex, ex); + }); + } + +} diff --git a/chapter09/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java b/chapter09/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java index ad0c0a4a..ad98f7e2 100644 --- a/chapter09/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java +++ b/chapter09/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java @@ -1,456 +1,456 @@ -package com.seaofnodes.simple.fuzzer;
-
-import com.seaofnodes.simple.Parser;
-import com.seaofnodes.simple.node.ScopeNode;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.Random;
-
-/**
- * Generate a pseudo random script.
- * This generator will generate the same script for two random number generators initialized with the same seed.
- * To generate valid code it implements the parser but instead of parsing it emits the statements and expressions.
- * It is guaranteed to terminate.
- */
-public class ScriptGenerator {
-
- /**
- * Number of spaces per indentation
- */
- private static final int INDENTATION = 4;
- /**
- * Valid characters for identifiers. The last 10 are not valid for the first character.
- */
- private static final String VAR_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789";
- /**
- * List of keywords not valid for identifiers.
- */
- private static final HashSet<String> KEYWORDS;
-
- static {
- try {
- KEYWORDS = FuzzerUtils.getFieldValue(new Parser(""), "KEYWORDS");
- } catch (Throwable e) {
- throw new ExceptionInInitializerError(e);
- }
- }
-
- /**
- * Flag for a statement that it does not pass on control flow to the next statement.
- */
- private static final int FLAG_STOP = 0x01;
- /**
- * Flag for a statement that it contains a final if statement without an else branch.
- */
- private static final int FLAG_IF_WITHOUT_ELSE = 0x02;
- /**
- * Binary operators.
- */
- private static final String[] BINARY_OP = {"==", "!=", "<", "<=", ">", ">=", "+", "-", "*", "/"};
- /**
- * Unary operators.
- */
- private static final String[] UNARY_OP = {"-"};
-
-
- /**
- * The random number generator used for random decisions.
- */
- private final Random random;
- /**
- * Indentation depth.
- */
- private int indentation = 0;
- /**
- * Output string builder
- */
- private final StringBuilder sb;
- /**
- * Number of nested loops
- */
- private int loopDepth = 0;
- /**
- * Current scope start in the variables list.
- */
- private int currScopeStart = 0;
- /**
- * The depth of blocks allowed.
- */
- private int depth = 20;
- /**
- * The depth of expressions allowed.
- */
- private int exprDepth = 30;
- /**
- * Current variables in scope.
- */
- private final ArrayList<String> variables = new ArrayList<>();
- /**
- * If this needs to generate valid scripts or is also allowed to generate invalid ones.
- */
- private final boolean generateValid;
- /**
- * If this script might be invalid.
- */
- private boolean maybeInvalid = false;
-
- /**
- *
- * @param random the random number generator.
- * @param sb the script will be written to this string builder.
- * @param generateValid if the script should be guaranteed valid or if it is allowed to contain bugs.
- */
- public ScriptGenerator(Random random, StringBuilder sb, boolean generateValid) {
- this.random = random;
- this.sb = sb;
- this.generateValid = generateValid;
- }
-
- /**
- * Random number with log distribution
- * @param max The maximum value
- * @return A random number in the range [0, max) with higher numbers being more uncommon.
- */
- private int randLog(int max) {
- max--;
- int n = 1<<max;
- var num = random.nextInt(n);
- for (int i=max-1; i>=0; i--) {
- if ((num & (1<<i))!=0) return max-i-1;
- }
- return max;
- }
-
- /**
- * Helper function to print indentation.
- * @return sb for chaining
- */
- private StringBuilder printIndentation() {
- return sb.repeat(' ', indentation);
- }
-
- /**
- * Generate a random name for variables. This might be invalid when it is a keyword.
- * @return The random name generated
- */
- private StringBuilder getRandomName() {
- int len = random.nextInt(30) + 1;
- StringBuilder sb = new StringBuilder(len);
- sb.append(VAR_CHARS.charAt(random.nextInt(VAR_CHARS.length()-10)));
- for (int i=1; i<len; i++)
- sb.append(VAR_CHARS.charAt(random.nextInt(VAR_CHARS.length())));
- return sb;
- }
-
- /**
- * Generate a variable name. This might be new random name or a name from an outer scope.
- * If the program is allowed to be invalid this can return keywords and already used variable names in the current scope.
- * @return The variable name.
- */
- private String getVarName() {
- if (currScopeStart > 0 && random.nextInt(10) > 7) {
- // Use a variable outside the current scope.
- var v = variables.get(random.nextInt(currScopeStart));
- if (variables.lastIndexOf(v) < currScopeStart) return v;
- if (!generateValid) {
- maybeInvalid = true;
- return v;
- }
- }
- // Generate a new random variable name
- StringBuilder sb = getRandomName();
- var v = sb.toString();
- if (!generateValid && (KEYWORDS.contains(v) || variables.lastIndexOf(v) >= currScopeStart)) {
- maybeInvalid = true;
- return v;
- }
- while (KEYWORDS.contains(v) || variables.lastIndexOf(v) >= currScopeStart) {
- sb.append(VAR_CHARS.charAt(random.nextInt(VAR_CHARS.length())));
- v = sb.toString();
- }
- return v;
- }
-
-
-
- /**
- * Generates a program and writes it into the string builder supplied in the constructor.
- * @return If the program is allowed to be invalid returns if something potentially invalid was generated.
- */
- public boolean genProgram() {
- this.maybeInvalid = false;
- variables.add(ScopeNode.ARG0);
- currScopeStart = variables.size();
- if ((genStatements() & FLAG_STOP) == 0) {
- if (generateValid || random.nextInt(10)<7) {
- genReturn();
- } else {
- this.maybeInvalid = true;
- }
- }
- return !this.maybeInvalid;
- }
-
- /**
- * Generate a list of statements.
- * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the last statement generated
- */
- public int genStatements() {
- var num = random.nextInt(10); // (30);
- for (int i=0; i<num; i++) {
- printIndentation();
- var stop = genStatement();
- sb.append("\n");
- if ((stop & FLAG_STOP) != 0) return stop;
- }
- return 0;
- }
-
- /**
- * Generate a single statement
- * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the generated statement
- */
- public int genStatement() {
- return switch (random.nextInt(10)) {
- case 1 -> genBlock();
- case 2 -> genIf();
- case 3 -> genWhile();
- case 4, 5 -> genDecl();
- case 6, 7, 8 -> genAssignment();
- default -> genExit();
- };
- }
-
- /**
- * Generate a statement for if, else and while blocks.
- * This is special since it disallows declarations and prefers to generate blocks.
- * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the generated statement
- */
- public int genStatementBlock() {
- indentation += INDENTATION;
- int res;
- if (depth == 0) {
- res = random.nextBoolean() ? genAssignment() : genExit();
- } else {
- depth--;
- res = switch (random.nextInt(10)) {
- case 1, 2, 3, 4, 5 -> {
- depth++;
- indentation -= INDENTATION;
- var ret = genBlock();
- indentation += INDENTATION;
- depth--;
- yield ret;
- }
- case 6 -> genIf();
- case 7 -> genWhile();
- case 8 -> genAssignment();
- default -> genExit();
- };
- depth++;
- }
- indentation -= INDENTATION;
- return res;
- }
-
- /**
- * Generate an exit. This can be return or break and continue when in a loop.
- * @return FLAG_STOP
- */
- public int genExit() {
- if (loopDepth == 0 && generateValid) return genReturn();
- return switch (random.nextInt(7)) {
- case 0,1,2 -> {
- if (loopDepth == 0) maybeInvalid = true;
- sb.append("continue;");
- yield FLAG_STOP;
- }
- case 3,4,5 -> {
- if (loopDepth == 0) maybeInvalid = true;
- sb.append("break;");
- yield FLAG_STOP;
- }
- default -> genReturn();
- };
- }
-
- /**
- * Generate a block statement. After a certain depth only empty blocks are generated to ensure termination of the generator.
- * @return flag FLAG_STOP for the generated statement
- */
- public int genBlock() {
- if (depth == 0) {
- sb.append("{}");
- return 0;
- }
- depth--;
- int oldCSS = currScopeStart;
- currScopeStart = variables.size();
- sb.append("{\n");
- indentation += INDENTATION;
- var stop = genStatements();
- indentation -= INDENTATION;
- printIndentation().append("}");
- while (variables.size() > currScopeStart)
- variables.removeLast();
- currScopeStart = oldCSS;
- depth++;
- return stop & ~FLAG_IF_WITHOUT_ELSE;
- }
-
- /**
- * Generate an if statement with an optional else block.
- * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the generated statement
- */
- public int genIf() {
- sb.append("if(");
- genExpression();
- sb.append(") ");
- var stop = genStatementBlock();
- if ((stop & FLAG_IF_WITHOUT_ELSE) == 0 && random.nextInt(10) > 3) {
- sb.append("\n");
- printIndentation().append("else ");
- stop &= genStatementBlock();
- } else {
- stop = FLAG_IF_WITHOUT_ELSE;
- }
- return stop;
- }
-
- /**
- * Generate a while loop.
- * @return 0
- */
- public int genWhile() {
- sb.append("while(");
- genExpression();
- sb.append(") ");
- loopDepth++;
- genStatementBlock();
- loopDepth--;
- return 0;
- }
-
- /**
- * Generate a declaration statement.
- * @return 0
- */
- public int genDecl() {
- String name = getVarName();
- sb.append("int ").append(name).append("=");
- genExpression();
- sb.append(";");
- variables.add(name);
- return 0;
- }
-
- /**
- * Generate an assignment statement.
- * @return 0
- */
- public int genAssignment() {
- if (variables.isEmpty()) return genDecl();
- genVariable();
- sb.append("=");
- genExpression();
- sb.append(";");
- return 0;
- }
-
- /**
- * Generate a return statement.
- * @return FLAG_STOP
- */
- public int genReturn() {
- sb.append("return ");
- genExpression();
- sb.append(";");
- return FLAG_STOP;
- }
-
-
- /**
- * Generate a binary expression.
- * This method does not care about operator precedence.
- */
- public void genExpression() {
- var num = randLog(10);
- while(num-->0) {
- genUnary();
- sb.append(BINARY_OP[random.nextInt(BINARY_OP.length)]);
- }
- genUnary();
- }
-
- /**
- * Generate a unary expression.
- */
- public void genUnary() {
- var num = randLog(6);
- while(num-->0) {
- sb.append(UNARY_OP[random.nextInt(UNARY_OP.length)]);
- }
- genPrimary();
- }
-
- /**
- * Generate a primary expression.
- */
- public void genPrimary() {
- switch (random.nextInt(10)) {
- case 0:
- sb.append("true");
- break;
- case 1:
- sb.append("false");
- break;
- case 2:
- if (exprDepth == 0) {
- // Ensure termination of the generator.
- if (random.nextBoolean()) {
- genNumber();
- } else {
- genVariable();
- }
- } else {
- sb.append("(");
- exprDepth--;
- genExpression();
- exprDepth++;
- sb.append(")");
- }
- break;
- case 3, 4, 5, 6:
- genNumber();
- break;
- default:
- genVariable();
- }
- }
-
- /**
- * Generate a number.
- */
- public void genNumber() {
- sb.append(random.nextInt(100));
- }
-
- /**
- * Generate a variable. If there are none generate a number instead.
- * If the program is allowed to be invalid a random name can be generated in some cases.
- */
- public void genVariable() {
- if (!generateValid && random.nextInt(100)>97) {
- sb.append(getRandomName());
- maybeInvalid = true;
- return;
- }
- if (variables.isEmpty()) {
- genNumber();
- } else {
- sb.append(variables.get(random.nextInt(variables.size())));
- }
- }
-
-}
+package com.seaofnodes.simple.fuzzer; + +import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.node.ScopeNode; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Random; + +/** + * Generate a pseudo random script. + * This generator will generate the same script for two random number generators initialized with the same seed. + * To generate valid code it implements the parser but instead of parsing it emits the statements and expressions. + * It is guaranteed to terminate. + */ +public class ScriptGenerator { + + /** + * Number of spaces per indentation + */ + private static final int INDENTATION = 4; + /** + * Valid characters for identifiers. The last 10 are not valid for the first character. + */ + private static final String VAR_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789"; + /** + * List of keywords not valid for identifiers. + */ + private static final HashSet<String> KEYWORDS; + + static { + try { + KEYWORDS = FuzzerUtils.getFieldValue(new Parser(""), "KEYWORDS"); + } catch (Throwable e) { + throw new ExceptionInInitializerError(e); + } + } + + /** + * Flag for a statement that it does not pass on control flow to the next statement. + */ + private static final int FLAG_STOP = 0x01; + /** + * Flag for a statement that it contains a final if statement without an else branch. + */ + private static final int FLAG_IF_WITHOUT_ELSE = 0x02; + /** + * Binary operators. + */ + private static final String[] BINARY_OP = {"==", "!=", "<", "<=", ">", ">=", "+", "-", "*", "/"}; + /** + * Unary operators. + */ + private static final String[] UNARY_OP = {"-"}; + + + /** + * The random number generator used for random decisions. + */ + private final Random random; + /** + * Indentation depth. + */ + private int indentation = 0; + /** + * Output string builder + */ + private final StringBuilder sb; + /** + * Number of nested loops + */ + private int loopDepth = 0; + /** + * Current scope start in the variables list. + */ + private int currScopeStart = 0; + /** + * The depth of blocks allowed. + */ + private int depth = 20; + /** + * The depth of expressions allowed. + */ + private int exprDepth = 30; + /** + * Current variables in scope. + */ + private final ArrayList<String> variables = new ArrayList<>(); + /** + * If this needs to generate valid scripts or is also allowed to generate invalid ones. + */ + private final boolean generateValid; + /** + * If this script might be invalid. + */ + private boolean maybeInvalid = false; + + /** + * + * @param random the random number generator. + * @param sb the script will be written to this string builder. + * @param generateValid if the script should be guaranteed valid or if it is allowed to contain bugs. + */ + public ScriptGenerator(Random random, StringBuilder sb, boolean generateValid) { + this.random = random; + this.sb = sb; + this.generateValid = generateValid; + } + + /** + * Random number with log distribution + * @param max The maximum value + * @return A random number in the range [0, max) with higher numbers being more uncommon. + */ + private int randLog(int max) { + max--; + int n = 1<<max; + var num = random.nextInt(n); + for (int i=max-1; i>=0; i--) { + if ((num & (1<<i))!=0) return max-i-1; + } + return max; + } + + /** + * Helper function to print indentation. + * @return sb for chaining + */ + private StringBuilder printIndentation() { + return sb.repeat(' ', indentation); + } + + /** + * Generate a random name for variables. This might be invalid when it is a keyword. + * @return The random name generated + */ + private StringBuilder getRandomName() { + int len = random.nextInt(30) + 1; + StringBuilder sb = new StringBuilder(len); + sb.append(VAR_CHARS.charAt(random.nextInt(VAR_CHARS.length()-10))); + for (int i=1; i<len; i++) + sb.append(VAR_CHARS.charAt(random.nextInt(VAR_CHARS.length()))); + return sb; + } + + /** + * Generate a variable name. This might be new random name or a name from an outer scope. + * If the program is allowed to be invalid this can return keywords and already used variable names in the current scope. + * @return The variable name. + */ + private String getVarName() { + if (currScopeStart > 0 && random.nextInt(10) > 7) { + // Use a variable outside the current scope. + var v = variables.get(random.nextInt(currScopeStart)); + if (variables.lastIndexOf(v) < currScopeStart) return v; + if (!generateValid) { + maybeInvalid = true; + return v; + } + } + // Generate a new random variable name + StringBuilder sb = getRandomName(); + var v = sb.toString(); + if (!generateValid && (KEYWORDS.contains(v) || variables.lastIndexOf(v) >= currScopeStart)) { + maybeInvalid = true; + return v; + } + while (KEYWORDS.contains(v) || variables.lastIndexOf(v) >= currScopeStart) { + sb.append(VAR_CHARS.charAt(random.nextInt(VAR_CHARS.length()))); + v = sb.toString(); + } + return v; + } + + + + /** + * Generates a program and writes it into the string builder supplied in the constructor. + * @return If the program is allowed to be invalid returns if something potentially invalid was generated. + */ + public boolean genProgram() { + this.maybeInvalid = false; + variables.add(ScopeNode.ARG0); + currScopeStart = variables.size(); + if ((genStatements() & FLAG_STOP) == 0) { + if (generateValid || random.nextInt(10)<7) { + genReturn(); + } else { + this.maybeInvalid = true; + } + } + return !this.maybeInvalid; + } + + /** + * Generate a list of statements. + * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the last statement generated + */ + public int genStatements() { + var num = random.nextInt(10); // (30); + for (int i=0; i<num; i++) { + printIndentation(); + var stop = genStatement(); + sb.append("\n"); + if ((stop & FLAG_STOP) != 0) return stop; + } + return 0; + } + + /** + * Generate a single statement + * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the generated statement + */ + public int genStatement() { + return switch (random.nextInt(10)) { + case 1 -> genBlock(); + case 2 -> genIf(); + case 3 -> genWhile(); + case 4, 5 -> genDecl(); + case 6, 7, 8 -> genAssignment(); + default -> genExit(); + }; + } + + /** + * Generate a statement for if, else and while blocks. + * This is special since it disallows declarations and prefers to generate blocks. + * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the generated statement + */ + public int genStatementBlock() { + indentation += INDENTATION; + int res; + if (depth == 0) { + res = random.nextBoolean() ? genAssignment() : genExit(); + } else { + depth--; + res = switch (random.nextInt(10)) { + case 1, 2, 3, 4, 5 -> { + depth++; + indentation -= INDENTATION; + var ret = genBlock(); + indentation += INDENTATION; + depth--; + yield ret; + } + case 6 -> genIf(); + case 7 -> genWhile(); + case 8 -> genAssignment(); + default -> genExit(); + }; + depth++; + } + indentation -= INDENTATION; + return res; + } + + /** + * Generate an exit. This can be return or break and continue when in a loop. + * @return FLAG_STOP + */ + public int genExit() { + if (loopDepth == 0 && generateValid) return genReturn(); + return switch (random.nextInt(7)) { + case 0,1,2 -> { + if (loopDepth == 0) maybeInvalid = true; + sb.append("continue;"); + yield FLAG_STOP; + } + case 3,4,5 -> { + if (loopDepth == 0) maybeInvalid = true; + sb.append("break;"); + yield FLAG_STOP; + } + default -> genReturn(); + }; + } + + /** + * Generate a block statement. After a certain depth only empty blocks are generated to ensure termination of the generator. + * @return flag FLAG_STOP for the generated statement + */ + public int genBlock() { + if (depth == 0) { + sb.append("{}"); + return 0; + } + depth--; + int oldCSS = currScopeStart; + currScopeStart = variables.size(); + sb.append("{\n"); + indentation += INDENTATION; + var stop = genStatements(); + indentation -= INDENTATION; + printIndentation().append("}"); + while (variables.size() > currScopeStart) + variables.removeLast(); + currScopeStart = oldCSS; + depth++; + return stop & ~FLAG_IF_WITHOUT_ELSE; + } + + /** + * Generate an if statement with an optional else block. + * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the generated statement + */ + public int genIf() { + sb.append("if("); + genExpression(); + sb.append(") "); + var stop = genStatementBlock(); + if ((stop & FLAG_IF_WITHOUT_ELSE) == 0 && random.nextInt(10) > 3) { + sb.append("\n"); + printIndentation().append("else "); + stop &= genStatementBlock(); + } else { + stop = FLAG_IF_WITHOUT_ELSE; + } + return stop; + } + + /** + * Generate a while loop. + * @return 0 + */ + public int genWhile() { + sb.append("while("); + genExpression(); + sb.append(") "); + loopDepth++; + genStatementBlock(); + loopDepth--; + return 0; + } + + /** + * Generate a declaration statement. + * @return 0 + */ + public int genDecl() { + String name = getVarName(); + sb.append("int ").append(name).append("="); + genExpression(); + sb.append(";"); + variables.add(name); + return 0; + } + + /** + * Generate an assignment statement. + * @return 0 + */ + public int genAssignment() { + if (variables.isEmpty()) return genDecl(); + genVariable(); + sb.append("="); + genExpression(); + sb.append(";"); + return 0; + } + + /** + * Generate a return statement. + * @return FLAG_STOP + */ + public int genReturn() { + sb.append("return "); + genExpression(); + sb.append(";"); + return FLAG_STOP; + } + + + /** + * Generate a binary expression. + * This method does not care about operator precedence. + */ + public void genExpression() { + var num = randLog(10); + while(num-->0) { + genUnary(); + sb.append(BINARY_OP[random.nextInt(BINARY_OP.length)]); + } + genUnary(); + } + + /** + * Generate a unary expression. + */ + public void genUnary() { + var num = randLog(6); + while(num-->0) { + sb.append(UNARY_OP[random.nextInt(UNARY_OP.length)]); + } + genPrimary(); + } + + /** + * Generate a primary expression. + */ + public void genPrimary() { + switch (random.nextInt(10)) { + case 0: + sb.append("true"); + break; + case 1: + sb.append("false"); + break; + case 2: + if (exprDepth == 0) { + // Ensure termination of the generator. + if (random.nextBoolean()) { + genNumber(); + } else { + genVariable(); + } + } else { + sb.append("("); + exprDepth--; + genExpression(); + exprDepth++; + sb.append(")"); + } + break; + case 3, 4, 5, 6: + genNumber(); + break; + default: + genVariable(); + } + } + + /** + * Generate a number. + */ + public void genNumber() { + sb.append(random.nextInt(100)); + } + + /** + * Generate a variable. If there are none generate a number instead. + * If the program is allowed to be invalid a random name can be generated in some cases. + */ + public void genVariable() { + if (!generateValid && random.nextInt(100)>97) { + sb.append(getRandomName()); + maybeInvalid = true; + return; + } + if (variables.isEmpty()) { + genNumber(); + } else { + sb.append(variables.get(random.nextInt(variables.size()))); + } + } + +} |
