diff options
Diffstat (limited to 'src/test/java/com/seaofnodes/simple/fuzzer')
4 files changed, 520 insertions, 135 deletions
diff --git a/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java b/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java index c3444093..7dcb88e5 100644 --- a/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java +++ b/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java @@ -1,11 +1,12 @@ package com.seaofnodes.simple.fuzzer; -import com.seaofnodes.simple.GraphEvaluator; import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.evaluator.Evaluator; import com.seaofnodes.simple.node.Node; import com.seaofnodes.simple.node.StopNode; import java.util.ArrayList; +import java.util.Objects; import java.util.Random; import java.util.function.Consumer; @@ -36,13 +37,15 @@ public class Fuzzer { * @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) { + private void recordException(Throwable e, String script, Consumer<String> reproducer, long seed) { for (var ex : exceptions) { if (FuzzerUtils.isExceptionFromSameCause(ex, e)) return; } exceptions.add(e); System.out.println("========== Stack =========="); e.printStackTrace(System.out); + System.out.println("========== Seed =========="); + System.out.println(seed); System.out.println("========== Code ==========="); System.out.println(script); System.out.println("========= Reduced ========="); @@ -51,20 +54,30 @@ public class Fuzzer { System.out.flush(); } + private static boolean neq(Object a, Object b) { + if (Objects.equals(a, b)) return false; + if (a instanceof Evaluator.Obj ea && b instanceof Evaluator.Obj eb) { + if (ea.struct() != eb.struct()) return true; + for(int i=0;i<ea.fields().length;i++) { + if (neq(ea.fields()[i], eb.fields()[i])) return true; + } + return false; + } + return true; + } + /** * 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 e1 Evaluator for the first graph + * @param e2 Evaluator for 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()); + private static void checkGraphs(Evaluator e1, Evaluator e2, long in) { + var r1 = e1.evaluate(in, EVAL_TIMEOUT); + var r2 = e2.evaluate(in, EVAL_TIMEOUT); + if (r1 == Evaluator.Status.TIMEOUT || r2 == Evaluator.Status.TIMEOUT) return; + if (neq(r1, r2)) + throw new RuntimeException("Different calculations values " + r1 + " vs " + r2); } /** @@ -91,9 +104,11 @@ public class Fuzzer { throw e1; } var stop2 = FuzzerUtils.parse(script, 456); - checkGraphs(stop1, stop2, 0); - checkGraphs(stop1, stop2, 1); - checkGraphs(stop1, stop2, 10); + var e1 = new Evaluator(stop1); + var e2 = new Evaluator(stop2); + checkGraphs(e1, e2, 0); + checkGraphs(e1, e2, 1); + checkGraphs(e1, e2, 10); } /** @@ -101,11 +116,11 @@ public class Fuzzer { * @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) { + private void check(String script, boolean valid, long seed) { try { runCheck(script, valid); } catch (Throwable e) { - recordException(e, script, s->runCheck(s, valid)); + recordException(e, script, s->runCheck(s, valid), seed); } } @@ -155,8 +170,8 @@ public class Fuzzer { 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); + var valid = new ScriptGenerator(rand, sb, true).genProgram(); + check(sb.toString(), valid, seed); } @@ -172,7 +187,7 @@ public class Fuzzer { public void fuzzPeepsRegression(long seed) { var rand = new Random(seed); var sb = new StringBuilder(); - var valid = new ScriptGenerator(rand, sb, false).genProgram(); + var valid = new ScriptGenerator(rand, sb, true).genProgram(); try { runCheck(sb.toString(), valid); } catch (Throwable e) { diff --git a/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java b/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java index 6488297b..f947a234 100644 --- a/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java +++ b/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java @@ -18,7 +18,7 @@ import java.util.BitSet; class FuzzerUtils { /** - * Get the value of a private field + * Get the val of a private field */ @SuppressWarnings("unchecked") public static <T> T getFieldValue(Object obj, String name) throws NoSuchFieldException, IllegalAccessException { @@ -76,10 +76,12 @@ class FuzzerUtils { } /** - * Check that eiter one of the objects is null or that both are equal. + * Check that the messages are similar. */ - private static boolean equalOrNull(Object a, Object b) { + private static boolean similarMessages(String a, String b) { if (a == null || b == null) return true; + a = a.replaceAll("'\\w+'", "'normalized'").replaceAll("variable \\w+", "variable normalized"); + b = b.replaceAll("'\\w+'", "'normalized'").replaceAll("variable \\w+", "variable normalized"); return a.equals(b); } @@ -98,7 +100,7 @@ class FuzzerUtils { 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 + if (!similarMessages(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 diff --git a/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java b/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java index 2ef0a3fd..3731b40c 100644 --- a/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java +++ b/src/test/java/com/seaofnodes/simple/fuzzer/Reducer.java @@ -1,7 +1,10 @@ package com.seaofnodes.simple.fuzzer; +import java.util.Arrays; +import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.regex.Matcher; import java.util.regex.Pattern; /** @@ -13,35 +16,45 @@ import java.util.regex.Pattern; */ public class Reducer { + private interface Replacer extends BiConsumer<StringBuilder, Matcher> {} + /** * Holder for the rewrite rules */ - private record Replace(Pattern p, String r) {} + private record Replace(Pattern pattern, boolean iterate, Replacer... replacements) { + Replace(Pattern p, Object... replacements) { + this(p, replacements[0] == Boolean.TRUE, Arrays.stream(replacements).filter(o->!(o instanceof Boolean)).map(Replace::from).toArray(Replacer[]::new)); + } + private static Replacer from(Object o) { + if (o instanceof Replacer r) return r; + if (o instanceof Integer i) return (s, m)->s.append(m.group(i)); + if (o instanceof String r) return (s, m)->s.append(r); + throw new IllegalArgumentException(); + } + } /** * List of rewrite rules. */ private static final Replace[] REPLACEMENTS = { + new Replace(Pattern.compile("\\b(?:[^\\W0]|\\w\\w+)\\b"), "0", "1"), + new Replace(Pattern.compile("(?<=\\n|^)[^\\n]+(?=\\n|$)"), true, ""), 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("\\b\\+-"), "-"), 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("\\{((?:[^{}]|\\{})+)(?:\\n[ \\t]*)?}"), "", "{}", 1), + new Replace(Pattern.compile("(?<=\\n|^)([ \\t]*)\\{\\n(.*?)\\n\\1}", Pattern.DOTALL), 2), + new Replace(Pattern.compile("\\(([^()]+)\\)"), 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+"), "\n"), new Replace(Pattern.compile("^\\n+|\\n+$"), ""), new Replace(Pattern.compile("else *"), ""), new Replace(Pattern.compile("\\bif\\([^()]+\\) *"), ""), new Replace(Pattern.compile("\\bwhile\\([^()]+\\) *"), ""), + new Replace(Pattern.compile("\\bstruct \\w+ \\{[^{}]*}"), "") }; /** @@ -60,24 +73,31 @@ public class Reducer { /** * Rewrites the script with one rewrite rule. */ - private String doReplacement(String script, Pattern pattern, String with) { + private String doReplacement(String script, Pattern pattern, Replacer[] replacements) { var matcher = pattern.matcher(script); var sb = new StringBuilder(); - int last = 0; - while (matcher.find()) { + var last = 0; + var skip = 0; + outer: + while (matcher.find(last + skip)) { + sb.append(script, last, matcher.start()); + last = matcher.end(); + skip = last == matcher.start() ? 1 : 0; var pos = sb.length(); - matcher.appendReplacement(sb, with); - var tail = sb.length(); - matcher.appendTail(sb); - if (tester.test(sb.toString())) { - sb.setLength(tail); - } else { + for (var replacement : replacements) { + replacement.accept(sb, matcher); + var tail = sb.length(); + sb.append(script, last, script.length()); + if (tester.test(sb.toString())) { + sb.setLength(tail); + continue outer; + } sb.setLength(pos); - sb.append(script, last, matcher.end()); } - last = matcher.end(); + last = matcher.start(); + skip = 1; } - matcher.appendTail(sb); + sb.append(script, last, script.length()); return sb.toString(); } @@ -85,13 +105,21 @@ public class Reducer { * 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; + var matcher = Pattern.compile("\\b([0-9a-zA-Z_]+) ([0-9a-zA-Z_]+)").matcher(script); + int[] num = {0, 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); + var type = matcher.group(1); + var var = matcher.group(2); + if (type.equals("new")) continue; + String prefix = "v"; + int t = 0; + if (type.equals("struct")) { + t = 1; + prefix = "s"; + } + if (var.matches(prefix+"\\d+")) continue; + while (script.contains(prefix+num[t])) num[t]++; + var n = script.replaceAll("\\b"+var+"\\b", prefix + num[t]); if (tester.test(n)) { script = n; } @@ -104,7 +132,11 @@ public class Reducer { */ private String doAllReplacements(String script) { for (var replace: REPLACEMENTS) { - script = doReplacement(script, replace.p, replace.r); + String old; + do { + old = script; + script = doReplacement(script, replace.pattern, replace.replacements); + } while (replace.iterate && !old.equals(script)); } return script; } diff --git a/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java b/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java index ad98f7e2..155063ab 100644 --- a/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java +++ b/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java @@ -6,6 +6,7 @@ import com.seaofnodes.simple.node.ScopeNode; import java.util.ArrayList; import java.util.HashSet; import java.util.Random; +import java.util.function.Predicate; /** * Generate a pseudo random script. @@ -15,6 +16,13 @@ import java.util.Random; */ public class ScriptGenerator { + private static final int MAX_STATEMENTS_PER_BLOCK = 5; + private static final int MAX_BINARY_EXPRESSIONS_PER_EXPRESSION = 10; + private static final int MAX_UNARY_EXPRESSIONS_PER_EXPRESSION = 6; + private static final int MAX_NAME_LENGTH = 10; + private static final int MAX_BLOCK_DEPTH = 4; + private static final int MAX_EXPRESSION_DEPTH = 10; + /** * Number of spaces per indentation */ @@ -53,6 +61,51 @@ public class ScriptGenerator { */ private static final String[] UNARY_OP = {"-"}; + private static class Type { + final String name; + Type(String name) {this.name=name;} + boolean isa(Type other) { return this == other; } + } + + private static class TypeStruct extends Type { + record Field (String name, Type type) {} + final Field[] fields; + final TypeNullable nullable = new TypeNullable(this); + TypeStruct(String name, Field[] fields) { + super(name); + this.fields = fields; + } + boolean isa(Type other) { return this == other || other == nullable; } + } + + private static class TypeNullable extends Type { + + final Type base; + + TypeNullable(Type base) { + super(base.name+"?"); + this.base = base; + } + + } + + private static final Type TYPE_INT = new Type("int"); + + + private static class Variable { + final String name; + final Type declared; + final Variable shadowing; + Type type; + boolean shadowed = false; + + Variable(String name, Type type, Variable var) { + this.name = name; + this.declared = type; + this.type = type; + this.shadowing = var; + } + } /** * The random number generator used for random decisions. @@ -77,24 +130,33 @@ public class ScriptGenerator { /** * The depth of blocks allowed. */ - private int depth = 20; + private int depth = MAX_BLOCK_DEPTH; /** * The depth of expressions allowed. */ - private int exprDepth = 30; + private int exprDepth = MAX_EXPRESSION_DEPTH; /** * Current variables in scope. */ - private final ArrayList<String> variables = new ArrayList<>(); + private final ArrayList<Variable> variables = new ArrayList<>(); + /** + * All defined structs. + */ + private final ArrayList<TypeStruct> structs = new ArrayList<>(); + /** + * Allow to declare structs. + */ + private boolean allowStructs = true; /** * If this needs to generate valid scripts or is also allowed to generate invalid ones. */ - private final boolean generateValid; + private boolean generateValid; /** * If this script might be invalid. */ private boolean maybeInvalid = false; + /** * * @param random the random number generator. @@ -135,7 +197,7 @@ public class ScriptGenerator { * @return The random name generated */ private StringBuilder getRandomName() { - int len = random.nextInt(30) + 1; + int len = random.nextInt(MAX_NAME_LENGTH) + 1; StringBuilder sb = new StringBuilder(len); sb.append(VAR_CHARS.charAt(random.nextInt(VAR_CHARS.length()-10))); for (int i=1; i<len; i++) @@ -144,6 +206,90 @@ public class ScriptGenerator { } /** + * Should invalid code be generated? + * @return True if invalid code should be generated + */ + private boolean generateInvalid() { + if (generateValid || random.nextInt(1000)>2) return false; + maybeInvalid = true; + return true; + } + + /** + * Lookup a variable by name + * @param name name of the variable + * @return The index of the variable in variables or -1 + */ + private int lookup(String name) { + for (int i=variables.size()-1; i>=0; i--) { + if (variables.get(i).name.equals(name)) return i; + } + return -1; + } + + /** + * Lookup a variable by name + * @param name name of the variable + * @return THe variable of null if not found + */ + private Variable lookupVar(String name) { + int idx = lookup(name); + return idx == -1 ? null : variables.get(idx); + } + + /** + * Add a variable to the current scope + * @param name The name of the variable + * @param type The declared type of the variable + */ + private void addVariable(String name, Type type) { + var old = lookupVar(name); + variables.add(new Variable(name, type, old)); + if (old != null) old.shadowed = true; + } + + /** + * Pop variables to level to + * @param to Level until which variables should be popped + */ + private void popVariables(int to) { + while (variables.size() > to) { + var v = variables.removeLast(); + if (v.shadowing != null) v.shadowing.shadowed = false; + } + } + + /** + * Get a random visible variable patching pred + * @param pred The predicate the variable needs to match + * @return A random visible variable matching pred or null if no variable mached pred. + */ + private Variable findVisibleVariablesMatching(Predicate<Variable> pred) { + int num=0; + for (var v:variables) { + if (!v.shadowed && pred.test(v)) num++; + } + if (num == 0) return null; + num = random.nextInt(num); + for (var v:variables) { + if (!v.shadowed && pred.test(v) && num-- == 0) return v; + } + throw new AssertionError(); + } + + /** + * Get a structure by name + * @param name Name of the structure + * @return The structure of null + */ + private TypeStruct getStruct(String name) { + for (var s:structs) { + if (s.name.equals(name)) return s; + } + return null; + } + + /** * 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. @@ -152,35 +298,67 @@ public class ScriptGenerator { 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 (lookup(v.name) < currScopeStart) return v.name; if (!generateValid) { maybeInvalid = true; - return v; + return v.name; } } // Generate a new random variable name StringBuilder sb = getRandomName(); var v = sb.toString(); - if (!generateValid && (KEYWORDS.contains(v) || variables.lastIndexOf(v) >= currScopeStart)) { + if (!generateValid && (KEYWORDS.contains(v) || lookup(v) >= currScopeStart)) { maybeInvalid = true; return v; } - while (KEYWORDS.contains(v) || variables.lastIndexOf(v) >= currScopeStart) { + while (KEYWORDS.contains(v) || lookup(v) >= currScopeStart) { sb.append(VAR_CHARS.charAt(random.nextInt(VAR_CHARS.length()))); v = sb.toString(); } return v; } + /** + * Get a random structure name + * @return A random structure name + */ + private String getStructName() { + if (!structs.isEmpty() && generateInvalid()) return structs.get(random.nextInt(structs.size())).name; + // Generate a new random struct name + StringBuilder sb = getRandomName(); + var v = sb.toString(); + if (!generateValid && (KEYWORDS.contains(v) || getStruct(v) != null)) { + maybeInvalid = true; + return v; + } + while (KEYWORDS.contains(v) || getStruct(v) != null) { + sb.append(VAR_CHARS.charAt(random.nextInt(VAR_CHARS.length()))); + v = sb.toString(); + } + return v; + } + /** + * Get a random type + * @return A random type + */ + private Type getType() { + if (structs.isEmpty() || random.nextBoolean()) return TYPE_INT; + var idx = random.nextInt(structs.size()); + TypeStruct struct = structs.get(idx); + if (random.nextBoolean()) return struct.nullable; + return struct; + } /** * 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() { + var oldGenerateValid = generateValid; + if (!generateValid && random.nextBoolean()) generateValid = true; this.maybeInvalid = false; - variables.add(ScopeNode.ARG0); + addVariable(ScopeNode.ARG0, TYPE_INT); currScopeStart = variables.size(); if ((genStatements() & FLAG_STOP) == 0) { if (generateValid || random.nextInt(10)<7) { @@ -189,6 +367,7 @@ public class ScriptGenerator { this.maybeInvalid = true; } } + generateValid = oldGenerateValid; return !this.maybeInvalid; } @@ -197,7 +376,7 @@ public class ScriptGenerator { * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the last statement generated */ public int genStatements() { - var num = random.nextInt(10); // (30); + var num = random.nextInt(MAX_STATEMENTS_PER_BLOCK * (depth+1)); for (int i=0; i<num; i++) { printIndentation(); var stop = genStatement(); @@ -212,29 +391,55 @@ public class ScriptGenerator { * @return flags FLAG_STOP and FLAG_IF_WITHOUT_ELSE for the generated statement */ public int genStatement() { - return switch (random.nextInt(10)) { + return switch (random.nextInt(12)) { + case 0 -> allowStructs || generateInvalid() ? genStruct() : genAssignment(); case 1 -> genBlock(); case 2 -> genIf(); - case 3 -> genWhile(); - case 4, 5 -> genDecl(); - case 6, 7, 8 -> genAssignment(); + case 3 -> genCountedLoop(); + case 4 -> genWhile(); + case 5, 6 -> genDecl(); + case 7, 8, 9 -> genAssignment(); + case 10 -> genNullCheck(); default -> genExit(); }; } /** + * Generate a new structure + * @return 0 + */ + public int genStruct() { + var name = getStructName(); + var fields = new TypeStruct.Field[generateInvalid() ? 0 : random.nextInt(10)]; + sb.append("struct ").append(name).append(" {\n"); + indentation += INDENTATION; + for (int i=0; i<fields.length; i++) { + var fieldName = getRandomName(); + var type = generateInvalid() ? getType() : TYPE_INT; + printIndentation().append(type.name).append(" ").append(fieldName).append(";\n"); + fields[i] = new TypeStruct.Field(fieldName.toString(), type); + } + indentation -= INDENTATION; + printIndentation().append("}"); + structs.add(new TypeStruct(name, fields)); + return 0; + } + + /** * 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() { + var oldAllowStructs = allowStructs; + allowStructs = false; indentation += INDENTATION; int res; if (depth == 0) { res = random.nextBoolean() ? genAssignment() : genExit(); } else { depth--; - res = switch (random.nextInt(10)) { + res = switch (random.nextInt(12)) { case 1, 2, 3, 4, 5 -> { depth++; indentation -= INDENTATION; @@ -244,13 +449,16 @@ public class ScriptGenerator { yield ret; } case 6 -> genIf(); - case 7 -> genWhile(); - case 8 -> genAssignment(); + case 7 -> genCountedLoop(); + case 8 -> genWhile(); + case 9 -> genAssignment(); + case 10 -> genNullCheck(); default -> genExit(); }; depth++; } indentation -= INDENTATION; + allowStructs = oldAllowStructs; return res; } @@ -284,18 +492,20 @@ public class ScriptGenerator { sb.append("{}"); return 0; } - depth--; - int oldCSS = currScopeStart; + var oldAllowStructs = allowStructs; + var oldCSS = currScopeStart; + allowStructs = false; currScopeStart = variables.size(); + depth--; sb.append("{\n"); indentation += INDENTATION; var stop = genStatements(); indentation -= INDENTATION; printIndentation().append("}"); - while (variables.size() > currScopeStart) - variables.removeLast(); - currScopeStart = oldCSS; + popVariables(currScopeStart); depth++; + currScopeStart = oldCSS; + allowStructs = oldAllowStructs; return stop & ~FLAG_IF_WITHOUT_ELSE; } @@ -305,7 +515,7 @@ public class ScriptGenerator { */ public int genIf() { sb.append("if("); - genExpression(); + genExpression(TYPE_INT); sb.append(") "); var stop = genStatementBlock(); if ((stop & FLAG_IF_WITHOUT_ELSE) == 0 && random.nextInt(10) > 3) { @@ -319,12 +529,39 @@ public class ScriptGenerator { } /** + * Generate a block with a null check + * if (nullable) { handle_nullable_as_non_nullable; } + */ + public int genNullCheck() { + var v = findVisibleVariablesMatching(va->va.declared instanceof TypeNullable); + if (v == null) return genIf(); + var n = (TypeNullable)v.declared; + boolean negate = random.nextBoolean(); + sb.append("if("); + if (negate) sb.append("!"); + sb.append(v.name).append(")"); + if (!negate) v.type = n.base; + var stop = genStatementBlock(); + v.type = n; + if ((stop & FLAG_IF_WITHOUT_ELSE) == 0 && random.nextInt(10) > 3) { + sb.append("\n"); + printIndentation().append("else "); + if (negate) v.type = n.base; + stop &= genStatementBlock(); + v.type = n; + } else { + stop = FLAG_IF_WITHOUT_ELSE; + } + return stop; + } + + /** * Generate a while loop. * @return 0 */ public int genWhile() { sb.append("while("); - genExpression(); + genExpression(TYPE_INT); sb.append(") "); loopDepth++; genStatementBlock(); @@ -333,15 +570,61 @@ public class ScriptGenerator { } /** + * Generate a counted loop of the form + * <code> + * { + * int varname = start_expr; + * while (varname < end_expr) loop_body; + * } + * </code> + * @return 0 + */ + public int genCountedLoop() { + if (depth == 0) return genWhile(); + var oldCSS = currScopeStart; + currScopeStart = variables.size(); + sb.append("{\n"); + indentation += INDENTATION; + String name = getVarName(); + printIndentation().append("int ").append(name).append("="); + genExpression(TYPE_INT); + sb.append(";\n"); + addVariable(name, TYPE_INT); + printIndentation().append("while(").append(name).append("<"); + genExpression(TYPE_INT); + sb.append(") {\n"); + indentation += INDENTATION; + depth--; + loopDepth++; + printIndentation().append(name).append("=").append(name).append("+"); + genExpression(TYPE_INT); + sb.append(";\n"); + genStatements(); + loopDepth--; + depth++; + indentation -= INDENTATION; + printIndentation().append("}\n"); + indentation -= INDENTATION; + printIndentation().append("}"); + popVariables(currScopeStart); + currScopeStart = oldCSS; + return 0; + } + + /** * Generate a declaration statement. * @return 0 */ public int genDecl() { - String name = getVarName(); - sb.append("int ").append(name).append("="); - genExpression(); + var type = getType(); + var name = getVarName(); + sb.append(generateInvalid() ? getRandomName() : type.name).append(" ").append(name); + if (!(type instanceof TypeNullable) || random.nextBoolean()) { + sb.append("="); + genExpression(type); + } sb.append(";"); - variables.add(name); + addVariable(name, type); return 0; } @@ -350,10 +633,30 @@ public class ScriptGenerator { * @return 0 */ public int genAssignment() { - if (variables.isEmpty()) return genDecl(); - genVariable(); + Type type; + Type declared; + if (generateInvalid()) { + sb.append(getRandomName()); + type = getType(); + declared = type; + } else { + var v = findVisibleVariablesMatching(va->true); + if (v == null) return genDecl(); + sb.append(v.name); + type = v.type; + declared = v.declared; + } + if (generateInvalid()) { + var name = getRandomName(); + sb.append(".").append(name); + declared = getType(); + } else if (type instanceof TypeStruct s && s.fields.length > 0 && random.nextBoolean()) { + var field = s.fields[random.nextInt(s.fields.length)]; + sb.append(".").append(field.name); + declared = field.type; + } sb.append("="); - genExpression(); + genExpression(declared); sb.append(";"); return 0; } @@ -363,94 +666,127 @@ public class ScriptGenerator { * @return FLAG_STOP */ public int genReturn() { + var type = getType(); sb.append("return "); - genExpression(); + genExpression(type); sb.append(";"); return FLAG_STOP; } - /** * Generate a binary expression. * This method does not care about operator precedence. */ - public void genExpression() { - var num = randLog(10); + public void genExpression(Type type) { + if (generateInvalid()) type = getType(); + if (type != TYPE_INT) { + genUnary(type); + return; + } + var num = randLog(MAX_BINARY_EXPRESSIONS_PER_EXPRESSION); while(num-->0) { - genUnary(); + genUnary(TYPE_INT); sb.append(BINARY_OP[random.nextInt(BINARY_OP.length)]); } - genUnary(); + genUnary(TYPE_INT); } /** * Generate a unary expression. */ - public void genUnary() { - var num = randLog(6); + public void genUnary(Type type) { + if (generateInvalid()) type = getType(); + if (type != TYPE_INT) { + genSuffix(type); + return; + } + var num = randLog(MAX_UNARY_EXPRESSIONS_PER_EXPRESSION); while(num-->0) { sb.append(UNARY_OP[random.nextInt(UNARY_OP.length)]); } - genPrimary(); + genSuffix(TYPE_INT); + } + + /** + * Generate + */ + public void genSuffix(Type type) { + if (generateInvalid()) type = getType(); + if (type == TYPE_INT && random.nextBoolean()) { + var t = getType(); + if (generateInvalid()) { + var field = getRandomName(); + genPrimary(t); + sb.append(".").append(field); + return; + } else if (t instanceof TypeStruct s && s.fields.length > 0) { + var field = s.fields[random.nextInt(s.fields.length)]; + if (field.type == TYPE_INT) { + genPrimary(t); + sb.append(".").append(field.name); + return; + } + } + } + genPrimary(type); } /** * 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(); + public void genPrimary(Type type) { + if (generateInvalid()) type = getType(); + var rand = random.nextInt(10); + if (rand == 0 && exprDepth != 0) { + sb.append("("); + exprDepth--; + genExpression(type); + exprDepth++; + sb.append(")"); + } else if (rand < 6) { + genVariable(type); + } else { + genConst(type); } } /** - * Generate a number. + * Generate a constant. */ - public void genNumber() { - sb.append(random.nextInt(100)); + public void genConst(Type type) { + if (generateInvalid()) type = getType(); + if (type == TYPE_INT) { + var rand = random.nextInt(10); + switch (rand) { + case 0 -> sb.append("true"); + case 1 -> sb.append("false"); + default -> sb.append(random.nextInt(1<<(rand-2))); + } + } else if (type instanceof TypeNullable n) { + if (random.nextBoolean()) { + sb.append("null"); + } else { + sb.append("new ").append(generateInvalid() ? getRandomName() : n.base.name); + } + } else { + sb.append("new ").append(generateInvalid() ? getRandomName() : type.name); + } } /** * 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) { + public void genVariable(Type type) { + if (generateInvalid()) { sb.append(getRandomName()); - maybeInvalid = true; return; } - if (variables.isEmpty()) { - genNumber(); - } else { - sb.append(variables.get(random.nextInt(variables.size()))); - } + if (generateInvalid()) type = getType(); + var t = type; + var v = findVisibleVariablesMatching(va->va.type.isa(t)); + if (v == null) genConst(type); + else sb.append(v.name); } } |
