Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions src/main/java/org/wpilib/gradlerio/graphgen/CodeBlockAnalyzer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package org.wpilib.gradlerio.graphgen;

import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.stmt.BlockStmt;
import com.github.javaparser.ast.stmt.Statement;

import java.util.*;
import java.util.stream.Collectors;

class CodeBlockAnalyzer {
/**
* Analyzes a code block for returned state machine states,
* and the conditions needed for the state to be returned.
* @param block The code block to analyze
* @return A map of state names to the conditions needed for that state to be returned
*/
static Map<String, String> analyze(BlockStmt block) {
var rawReturns = new HashMap<String, List<String>>();
analyzeStatement(block, "true", rawReturns);
return rawReturns.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> Utils.joinWithOr(entry.getValue())
));
}

private static List<String> analyzeStatement(
Statement stmt,
String pathCondition,
Map<String, List<String>> returnsMap
) {
var fallThroughConditions = new ArrayList<String>();

if (stmt.isBlockStmt()) {
var currentPaths = Collections.singletonList(pathCondition);
for (var subStmt: stmt.asBlockStmt().getStatements()) {
var nextPaths = new ArrayList<String>();
for (var path: currentPaths) {
nextPaths.addAll(analyzeStatement(subStmt, path, returnsMap));
}
currentPaths = nextPaths;
if (currentPaths.isEmpty()) {
break; // Block always returns before hitting subsequent lines
}
}
fallThroughConditions.addAll(currentPaths);
} else if (stmt.isReturnStmt()) {
var returnExpr = stmt.asReturnStmt().getExpression();
if (returnExpr.isEmpty()) return List.of();
ExpressionAnalyzer.analyze(returnExpr.get(), pathCondition, returnsMap);
} else if (stmt.isIfStmt()) {
var ifStmt = stmt.asIfStmt();
var conditionStr = ifStmt.getCondition().toString();
var thenCondition = Utils.joinWithAnd(pathCondition, conditionStr);
var elseCondition = Utils.joinWithAnd(pathCondition, Utils.negate(conditionStr));

fallThroughConditions.addAll(analyzeStatement(ifStmt.getThenStmt(), thenCondition, returnsMap));
if (ifStmt.getElseStmt().isPresent()) {
fallThroughConditions.addAll(analyzeStatement(ifStmt.getElseStmt().get(), elseCondition, returnsMap));
} else {
fallThroughConditions.add(elseCondition);
}
} else if (stmt.isSwitchStmt()) {
var switchStmt = stmt.asSwitchStmt();
var selector = switchStmt.getSelector().toString();
var switchExitPaths = new ArrayList<String>();
var pendingFallThrough = new ArrayList<String>();
var selectorMatches = new ArrayList<String>();

for (var entry: switchStmt.getEntries()) {
List<String> labels = entry.getLabels().stream().map(Node::toString).toList();
String entryDirectCondition;

if (labels.isEmpty()) {
// 'default' case handles whatever hasn't matched prior explicit conditions
if (selectorMatches.isEmpty()) {
entryDirectCondition = pathCondition;
} else {
var combinedMatches = Utils.joinWithOr(selectorMatches);
entryDirectCondition = Utils.joinWithAnd(pathCondition, Utils.negate(combinedMatches));
}
} else {
var matchCondition = Utils.joinWithOr(
labels.stream().map(label -> selector + " == " + label).toList()
);
entryDirectCondition = Utils.joinWithAnd(pathCondition, matchCondition);
selectorMatches.add(matchCondition);
}

// Current case entry path context = Direct match path + pending fall-through paths from previous case
var caseIncomingPaths = new ArrayList<String>();
caseIncomingPaths.add(entryDirectCondition);
caseIncomingPaths.addAll(pendingFallThrough);
pendingFallThrough.clear(); // Consumed by this case entry

// Process internal statement sequences within this switch branch
var currentCasePaths = caseIncomingPaths;
for (var subStmt: entry.getStatements()) {
if (subStmt.isBreakStmt()) {
// 'break' safely escapes the switch completely
switchExitPaths.addAll(currentCasePaths);
currentCasePaths = new ArrayList<>();
break;
}
var nextCasePaths = new ArrayList<String>();
for (var path: currentCasePaths) {
nextCasePaths.addAll(analyzeStatement(subStmt, path, returnsMap));
}
currentCasePaths = nextCasePaths;
if (currentCasePaths.isEmpty()) break;
}

// Anything still active cascades directly to the next case block
pendingFallThrough.addAll(currentCasePaths);
}

// Clean up left over paths
switchExitPaths.addAll(pendingFallThrough);

// If no default branch existed, values not explicit in cases fall through past the switch block
boolean hasDefault = switchStmt.getEntries().stream().anyMatch(e -> e.getLabels().isEmpty());
if (!hasDefault && !selectorMatches.isEmpty()) {
var combinedMatches = Utils.joinWithOr(selectorMatches);
switchExitPaths.add(Utils.joinWithAnd(pathCondition, Utils.negate(combinedMatches)));
}
fallThroughConditions.addAll(switchExitPaths);
} else {
fallThroughConditions.add(pathCondition);
}

return fallThroughConditions;
}
}
27 changes: 27 additions & 0 deletions src/main/java/org/wpilib/gradlerio/graphgen/ErrorLogger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.wpilib.gradlerio.graphgen;

import com.github.javaparser.ast.Node;

public class ErrorLogger {
private static String currentFilePath = "unknown file";

static void setFilePath(String path) {
currentFilePath = path;
}

/**
* A utility method that logs the line number and the file path
* that an error originates from, in addition to the error message.
* This serves to supplement the lack of stacktrace support within JavaParser.
* @param message The error message to throw
* @param node The location (statement or expression) that the error originates from
*/
static void throwError(String message, Node node) {
var begin = node.getBegin();
if (begin.isPresent()) {
throw new RuntimeException(currentFilePath + ": Line " + begin.get().line + ": " + message);
} else {
throw new RuntimeException(currentFilePath + ": Line " + message);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package org.wpilib.gradlerio.graphgen;

import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.SwitchExpr;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

class ExpressionAnalyzer {
/**
* Analyzes an inline expression for returned state machine states,
* and the conditions needed for the state to be returned.
* @param expr The expression to analyze
* @return A map of state names to the conditions needed for that state to be returned
*/
static Map<String, String> analyze(Expression expr) {
var rawReturns = new HashMap<String, List<String>>();
ExpressionAnalyzer.analyze(expr, "true", rawReturns);
return rawReturns.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> Utils.joinWithOr(entry.getValue())
));
}

// A mutable variant of analyze() used internally by CodeBlockAnalyzer.
static void analyze(
Expression expr,
String pathCondition,
Map<String, List<String>> returnsMap
) {
if (expr.isSwitchExpr()) {
handleSwitch(expr.asSwitchExpr(), pathCondition, returnsMap);
} else if (expr.isConditionalExpr()) {
var conditional = expr.asConditionalExpr();
var condition = conditional.getCondition().toString();
analyze(conditional.getThenExpr(), Utils.joinWithAnd(pathCondition, condition), returnsMap);
analyze(conditional.getElseExpr(), Utils.joinWithAnd(pathCondition, Utils.negate(condition)), returnsMap);
} else {
returnsMap.computeIfAbsent(expr.toString(), k -> new ArrayList<>()).add(pathCondition);
}
}

private static void handleSwitch(
SwitchExpr sw,
String pathCondition,
Map<String, List<String>> returnsMap
) {
var selector = sw.getSelector().toString();
var entries = sw.getEntries();
var defaultClauses = new ArrayList<String>();
for (var entry: entries) {
String matchCondition;
if (entry.getLabels().isEmpty()) {
matchCondition = Utils.joinWithAnd(defaultClauses);
} else {
// One or more comma-separated labels (arrow style) or stacked case: lines
matchCondition = Utils.joinWithOr(
entry.getLabels().stream().map(label -> selector + " == " + label).toList()
);
defaultClauses.add(Utils.negate(matchCondition));
}

var stmt = entry.getStatements().get(0);
if (stmt.isBlockStmt()) {
ErrorLogger.throwError("Yield Statements within switch blocks are not supported for @MakeStateMachineGraph.", stmt);
}
var expression = stmt.asExpressionStmt().getExpression();
var condition = Utils.joinWithAnd(pathCondition, matchCondition);
if (expression.isConditionalExpr() || expression.isSwitchExpr()) {
analyze(expression, condition, returnsMap);
} else {
var varName = expression.toString().trim();
returnsMap.computeIfAbsent(varName, k -> new ArrayList<>()).add(condition);
}
}
}
}
Loading