com.graphhopper.routing.weighting.custom.CustomModelParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of graphhopper-core Show documentation
Show all versions of graphhopper-core Show documentation
GraphHopper is a fast and memory efficient Java road routing engine
working seamlessly with OpenStreetMap data.
/*
* Licensed to GraphHopper GmbH under one or more contributor
* license agreements. See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*
* GraphHopper GmbH licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.graphhopper.routing.weighting.custom;
import com.graphhopper.json.Statement;
import com.graphhopper.routing.ev.*;
import com.graphhopper.routing.util.EncodingManager;
import com.graphhopper.routing.weighting.TurnCostProvider;
import com.graphhopper.util.*;
import com.graphhopper.util.shapes.BBox;
import com.graphhopper.util.shapes.Polygon;
import org.codehaus.commons.compiler.CompileException;
import org.codehaus.commons.compiler.Location;
import org.codehaus.commons.compiler.io.Readers;
import org.codehaus.janino.Scanner;
import org.codehaus.janino.*;
import org.codehaus.janino.util.DeepCopier;
import org.locationtech.jts.geom.Polygonal;
import org.locationtech.jts.geom.prep.PreparedPolygon;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import static com.graphhopper.json.Statement.Keyword.IF;
public class CustomModelParser {
private static final AtomicLong longVal = new AtomicLong(1);
static final String IN_AREA_PREFIX = "in_";
static final String BACKWARD_PREFIX = "backward_";
private static final boolean JANINO_DEBUG = Boolean.getBoolean(Scanner.SYSTEM_PROPERTY_SOURCE_DEBUGGING_ENABLE);
private static final String SCRIPT_FILE_DIR = System.getProperty(Scanner.SYSTEM_PROPERTY_SOURCE_DEBUGGING_DIR, "./src/main/java/com/graphhopper/routing/weighting/custom");
// Without a cache the class creation takes 10-40ms which makes routingLM8 requests 20% slower on average.
// CH requests and preparation is unaffected as cached weighting from preparation is used.
// Use accessOrder==true to remove oldest accessed entry, not oldest inserted.
private static final int CACHE_SIZE = Integer.getInteger("graphhopper.custom_weighting.cache_size", 1000);
private static final Map> CACHE = Collections.synchronizedMap(
new LinkedHashMap<>(CACHE_SIZE, 0.75f, true) {
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > CACHE_SIZE;
}
});
// This internal cache ensures that the "internal" Weighting classes specified in the profiles, are never removed regardless
// of how frequent other Weightings are created and accessed. We only need to synchronize the get and put methods alone.
// E.g. we do not care for the race condition where two identical classes are requested and one of them is overwritten.
// TODO perf compare with ConcurrentHashMap, but I guess, if there is a difference at all, it is not big for small maps
private static final Map> INTERNAL_CACHE = Collections.synchronizedMap(new HashMap<>());
private CustomModelParser() {
// utility class
}
/**
* This method creates a weighting from a CustomModel that must limit the speed. Either as an
* unconditional statement { "if": "true", "limit_to": "car_average_speed" }
or as
* an if-else block.
*/
public static CustomWeighting createWeighting(EncodedValueLookup lookup, TurnCostProvider turnCostProvider, CustomModel customModel) {
if (customModel == null)
throw new IllegalStateException("CustomModel cannot be null");
CustomWeighting.Parameters parameters = createWeightingParameters(customModel, lookup);
return new CustomWeighting(turnCostProvider, parameters);
}
/**
* This method compiles a new subclass of CustomWeightingHelper composed from the provided CustomModel caches this
* and returns an instance.
*/
public static CustomWeighting.Parameters createWeightingParameters(CustomModel customModel, EncodedValueLookup lookup) {
String key = customModel.toString();
if (key.length() > 100_000)
throw new IllegalArgumentException("Custom Model too big: " + key.length());
Class> clazz = customModel.isInternal() ? INTERNAL_CACHE.get(key) : null;
if (CACHE_SIZE > 0 && clazz == null)
clazz = CACHE.get(key);
if (clazz == null) {
clazz = createClazz(customModel, lookup);
if (customModel.isInternal()) {
INTERNAL_CACHE.put(key, clazz);
if (INTERNAL_CACHE.size() > 100) {
CACHE.putAll(INTERNAL_CACHE);
INTERNAL_CACHE.clear();
LoggerFactory.getLogger(CustomModelParser.class).warn("Internal cache must stay small but was "
+ INTERNAL_CACHE.size() + ". Cleared it. Misuse of CustomModel::internal?");
}
} else if (CACHE_SIZE > 0) {
CACHE.put(key, clazz);
}
}
try {
// The class does not need to be thread-safe as we create an instance per request
CustomWeightingHelper prio = (CustomWeightingHelper) clazz.getDeclaredConstructor().newInstance();
prio.init(customModel, lookup, CustomModel.getAreasAsMap(customModel.getAreas()));
return new CustomWeighting.Parameters(
prio::getSpeed, prio::calcMaxSpeed,
prio::getPriority, prio::calcMaxPriority,
customModel.getDistanceInfluence() == null ? 0 : customModel.getDistanceInfluence(),
customModel.getHeadingPenalty() == null ? Parameters.Routing.DEFAULT_HEADING_PENALTY : customModel.getHeadingPenalty());
} catch (ReflectiveOperationException ex) {
throw new IllegalArgumentException("Cannot compile expression " + ex.getMessage(), ex);
}
}
/**
* This method does the following:
*
* -
* 1. parse the value expressions (RHS) to know about additional encoded values ('findVariables')
* and check for multiplications with negative values.
*
* - 2. parse conditional expression of priority and speed statements -> done in ConditionalExpressionVisitor (don't parse RHS expressions again)
*
* - 3. create class template as String, inject the created statements and create the Class
*
*
*/
private static Class> createClazz(CustomModel customModel, EncodedValueLookup lookup) {
try {
Set priorityVariables = ValueExpressionVisitor.findVariables(customModel.getPriority(), lookup);
List priorityStatements = createGetPriorityStatements(priorityVariables, customModel, lookup);
if (customModel.getSpeed().isEmpty())
throw new IllegalArgumentException("At least one initial statement under 'speed' is required.");
List firstBlock = splitIntoBlocks(customModel.getSpeed()).get(0);
if (firstBlock.size() > 1) {
Statement lastSt = firstBlock.get(firstBlock.size() - 1);
if (lastSt.getOperation() != Statement.Op.LIMIT || lastSt.getKeyword() != Statement.Keyword.ELSE)
throw new IllegalArgumentException("The first block needs to end with an 'else' (or contain a single unconditional 'if' statement).");
} else {
Statement firstSt = firstBlock.get(0);
if (!"true".equals(firstSt.getCondition()) || firstSt.getOperation() != Statement.Op.LIMIT || firstSt.getKeyword() != Statement.Keyword.IF)
throw new IllegalArgumentException("The first block needs to contain a single unconditional 'if' statement (or end with an 'else').");
}
Set speedVariables = ValueExpressionVisitor.findVariables(customModel.getSpeed(), lookup);
List speedStatements = createGetSpeedStatements(speedVariables, customModel, lookup);
// Create different class name, which is required only for debugging.
// TODO does it improve performance too? I.e. it could be that the JIT is confused if different classes
// have the same name and it mixes performance stats. See https://github.com/janino-compiler/janino/issues/137
long counter = longVal.incrementAndGet();
String classTemplate = createClassTemplate(counter, priorityVariables, speedVariables, lookup, CustomModel.getAreasAsMap(customModel.getAreas()));
Java.CompilationUnit cu = (Java.CompilationUnit) new Parser(new Scanner("source", new StringReader(classTemplate))).
parseAbstractCompilationUnit();
cu = injectStatements(priorityStatements, speedStatements, cu);
SimpleCompiler sc = createCompiler(counter, cu);
return sc.getClassLoader().loadClass("com.graphhopper.routing.weighting.custom.JaninoCustomWeightingHelperSubclass" + counter);
} catch (Exception ex) {
String errString = "Cannot compile expression";
throw new IllegalArgumentException(errString + ": " + ex.getMessage(), ex);
}
}
public static List findVariablesForEncodedValuesString(CustomModel model, NameValidator nameValidator, ClassHelper classHelper) {
Set variables = new LinkedHashSet<>();
// avoid parsing exception for backward_xy or in_xy ...
NameValidator nameValidatorIntern = s -> {
// some literals are no variables and would throw an exception (encoded value not found)
if (Character.isUpperCase(s.charAt(0)) || s.startsWith(BACKWARD_PREFIX) || s.startsWith(IN_AREA_PREFIX))
return true;
if (nameValidator.isValid(s)) {
variables.add(s);
return true;
}
return false;
};
findVariablesForEncodedValuesString(model.getPriority(), nameValidatorIntern, classHelper);
findVariablesForEncodedValuesString(model.getSpeed(), nameValidatorIntern, classHelper);
return new ArrayList<>(variables);
}
private static void findVariablesForEncodedValuesString(List statements, NameValidator nameValidator, ClassHelper classHelper) {
List> blocks = CustomModelParser.splitIntoBlocks(statements);
for (List block : blocks) {
for (Statement statement : block) {
// ignore potential problems; collect only variables in this step
ConditionalExpressionVisitor.parse(statement.getCondition(), nameValidator, classHelper);
ValueExpressionVisitor.parse(statement.getValue(), nameValidator);
}
}
}
/**
* Splits the specified list into several list of statements starting with if
*/
static List> splitIntoBlocks(List statements) {
List> result = new ArrayList<>();
List block = null;
for (Statement st : statements) {
if (IF.equals(st.getKeyword())) result.add(block = new ArrayList<>());
if (block == null)
throw new IllegalArgumentException("Every block must start with an if-statement");
block.add(st);
}
return result;
}
/**
* Parse the expressions from CustomModel relevant for the method getSpeed - see createClassTemplate.
*
* @return the created statements (parsed expressions)
*/
private static List createGetSpeedStatements(Set speedVariables,
CustomModel customModel, EncodedValueLookup lookup) throws Exception {
List speedStatements = new ArrayList<>(verifyExpressions(new StringBuilder(),
"speed entry", speedVariables, customModel.getSpeed(), lookup));
String speedMethodStartBlock = "double value = " + CustomWeightingHelper.GLOBAL_MAX_SPEED + ";\n";
// potentially we fetch EncodedValues twice (one time here and one time for priority)
for (String arg : speedVariables) {
speedMethodStartBlock += getVariableDeclaration(lookup, arg);
}
speedStatements.addAll(0, new Parser(new org.codehaus.janino.Scanner("getSpeed", new StringReader(speedMethodStartBlock))).
parseBlockStatements());
return speedStatements;
}
/**
* Parse the expressions from CustomModel relevant for the method getPriority - see createClassTemplate.
*
* @return the created statements (parsed expressions)
*/
private static List createGetPriorityStatements(Set priorityVariables,
CustomModel customModel, EncodedValueLookup lookup) throws Exception {
List priorityStatements = new ArrayList<>(verifyExpressions(new StringBuilder(),
"priority entry", priorityVariables, customModel.getPriority(), lookup));
String priorityMethodStartBlock = "double value = " + CustomWeightingHelper.GLOBAL_PRIORITY + ";\n";
for (String arg : priorityVariables) {
priorityMethodStartBlock += getVariableDeclaration(lookup, arg);
}
priorityStatements.addAll(0, new Parser(new org.codehaus.janino.Scanner("getPriority", new StringReader(priorityMethodStartBlock))).
parseBlockStatements());
return priorityStatements;
}
/**
* For the methods getSpeed and getPriority we declare variables that contain the encoded value of the current edge
* or if an area contains the current edge.
*/
private static String getVariableDeclaration(EncodedValueLookup lookup, final String arg) {
if (lookup.hasEncodedValue(arg)) {
EncodedValue enc = lookup.getEncodedValue(arg, EncodedValue.class);
return getReturnType(enc) + " " + arg + " = (" + getReturnType(enc) + ") (reverse ? " +
"edge.getReverse((" + getInterface(enc) + ") this." + arg + "_enc) : " +
"edge.get((" + getInterface(enc) + ") this." + arg + "_enc));\n";
} else if (arg.startsWith(BACKWARD_PREFIX)) {
final String argSubstr = arg.substring(BACKWARD_PREFIX.length());
if (lookup.hasEncodedValue(argSubstr)) {
EncodedValue enc = lookup.getEncodedValue(argSubstr, EncodedValue.class);
return getReturnType(enc) + " " + arg + " = (" + getReturnType(enc) + ") (reverse ? " +
"edge.get((" + getInterface(enc) + ") this." + argSubstr + "_enc) : " +
"edge.getReverse((" + getInterface(enc) + ") this." + argSubstr + "_enc));\n";
} else {
throw new IllegalArgumentException("Not supported for backward: " + argSubstr);
}
} else if (arg.startsWith(IN_AREA_PREFIX)) {
return "";
} else {
throw new IllegalArgumentException("Not supported " + arg);
}
}
/**
* @return the interface as string of the provided EncodedValue, e.g. IntEncodedValue (only interface) or
* BooleanEncodedValue (first interface). For StringEncodedValue we return IntEncodedValue to return the index
* instead of the String for faster comparison.
*/
private static String getInterface(EncodedValue enc) {
if (enc instanceof StringEncodedValue) return IntEncodedValue.class.getSimpleName();
if (enc.getClass().getInterfaces().length == 0) return enc.getClass().getSimpleName();
return enc.getClass().getInterfaces()[0].getSimpleName();
}
private static String getReturnType(EncodedValue encodedValue) {
// order is important
if (encodedValue instanceof EnumEncodedValue) {
Class cl = ((EnumEncodedValue) encodedValue).getEnumType();
// use getSimpleName for inbuilt EncodedValues and more readability of generated source
return cl.getPackage().equals(EnumEncodedValue.class.getPackage()) ? cl.getSimpleName() : cl.getName();
}
if (encodedValue instanceof StringEncodedValue) return "int"; // we use indexOf
if (encodedValue instanceof DecimalEncodedValue) return "double";
if (encodedValue instanceof BooleanEncodedValue) return "boolean";
if (encodedValue instanceof IntEncodedValue) return "int";
throw new IllegalArgumentException("Unsupported EncodedValue: " + encodedValue.getClass());
}
/**
* Create the class source file from the detected variables (priorityVariables and speedVariables). We assume that
* these variables are safe although they are user input because we collected them from parsing via Janino. This
* means that the source file is free from user input and could be directly compiled. Before we do this we still
* have to inject that parsed and safe user expressions in a later step.
*/
private static String createClassTemplate(long counter,
Set priorityVariables, Set speedVariables,
EncodedValueLookup lookup, Map areas) {
final StringBuilder importSourceCode = new StringBuilder("import com.graphhopper.routing.ev.*;\n");
importSourceCode.append("import java.util.Map;\n");
importSourceCode.append("import " + CustomModel.class.getName() + ";\n");
final StringBuilder classSourceCode = new StringBuilder(100);
boolean includedAreaImports = false;
final StringBuilder initSourceCode = new StringBuilder("this.lookup = lookup;\n");
initSourceCode.append("this.customModel = customModel;\n");
Set set = new HashSet<>();
for (String prioVar : priorityVariables)
set.add(prioVar.startsWith(BACKWARD_PREFIX) ? prioVar.substring(BACKWARD_PREFIX.length()) : prioVar);
for (String speedVar : speedVariables)
set.add(speedVar.startsWith(BACKWARD_PREFIX) ? speedVar.substring(BACKWARD_PREFIX.length()) : speedVar);
for (String arg : set) {
if (lookup.hasEncodedValue(arg)) {
EncodedValue enc = lookup.getEncodedValue(arg, EncodedValue.class);
classSourceCode.append("protected " + getInterface(enc) + " " + arg + "_enc;\n");
initSourceCode.append("this." + arg + "_enc = (" + getInterface(enc)
+ ") lookup.getEncodedValue(\"" + arg + "\", EncodedValue.class);\n");
} else if (arg.startsWith(IN_AREA_PREFIX)) {
if (!includedAreaImports) {
importSourceCode.append("import " + BBox.class.getName() + ";\n");
importSourceCode.append("import " + GHUtility.class.getName() + ";\n");
importSourceCode.append("import " + PreparedPolygon.class.getName() + ";\n");
importSourceCode.append("import " + Polygonal.class.getName() + ";\n");
importSourceCode.append("import " + JsonFeature.class.getName() + ";\n");
importSourceCode.append("import " + Polygon.class.getName() + ";\n");
includedAreaImports = true;
}
if (!JsonFeature.isValidId(arg))
throw new IllegalArgumentException("Area has invalid name: " + arg);
String id = arg.substring(IN_AREA_PREFIX.length());
JsonFeature feature = areas.get(id);
if (feature == null)
throw new IllegalArgumentException("Area '" + id + "' wasn't found");
if (feature.getGeometry() == null)
throw new IllegalArgumentException("Area '" + id + "' does not contain a geometry");
if (!(feature.getGeometry() instanceof Polygonal))
throw new IllegalArgumentException("Currently only type=Polygon is supported for areas but was " + feature.getGeometry().getGeometryType());
if (feature.getBBox() != null)
throw new IllegalArgumentException("Bounding box of area " + id + " must be empty");
classSourceCode.append("protected " + Polygon.class.getSimpleName() + " " + arg + ";\n");
initSourceCode.append("JsonFeature feature_" + id + " = (JsonFeature) areas.get(\"" + id + "\");\n");
initSourceCode.append("this." + arg + " = new Polygon(new PreparedPolygon((Polygonal) feature_" + id + ".getGeometry()));\n");
} else {
if (!arg.startsWith(IN_AREA_PREFIX))
throw new IllegalArgumentException("Variable not supported: " + arg);
}
}
return ""
+ "package com.graphhopper.routing.weighting.custom;\n"
+ "import " + CustomWeightingHelper.class.getName() + ";\n"
+ "import " + EncodedValueLookup.class.getName() + ";\n"
+ "import " + EdgeIteratorState.class.getName() + ";\n"
+ importSourceCode
+ "\npublic class JaninoCustomWeightingHelperSubclass" + counter + " extends " + CustomWeightingHelper.class.getSimpleName() + " {\n"
+ classSourceCode
+ " @Override\n"
+ " public void init(CustomModel customModel, EncodedValueLookup lookup, Map areas) {\n"
+ initSourceCode
+ " }\n\n"
// we need these placeholder methods so that the hooks in DeepCopier are invoked
+ " @Override\n"
+ " public double getPriority(EdgeIteratorState edge, boolean reverse) {\n"
+ " return 1; //will be overwritten by code injected in DeepCopier\n"
+ " }\n"
+ " @Override\n"
+ " public double getSpeed(EdgeIteratorState edge, boolean reverse) {\n"
+ " return 1; //will be overwritten by code injected in DeepCopier\n"
+ " }\n"
+ "}";
}
/**
* This method does:
* 1. check user expressions via Parser.parseConditionalExpression and only allow whitelisted variables and methods.
* 2. while this check it also guesses the variable names and stores it in createObjects
* 3. creates if-then-elseif expressions from the checks and returns them as BlockStatements
*
* @return the created if-then, else and elseif statements
*/
private static List verifyExpressions(StringBuilder expressions, String info, Set createObjects,
List list, EncodedValueLookup lookup) throws Exception {
// allow variables, all encoded values, constants and special variables like in_xyarea or backward_car_access
NameValidator nameInConditionValidator = name -> lookup.hasEncodedValue(name)
|| name.toUpperCase(Locale.ROOT).equals(name) || name.startsWith(IN_AREA_PREFIX)
|| name.startsWith(BACKWARD_PREFIX) && lookup.hasEncodedValue(name.substring(BACKWARD_PREFIX.length()));
ClassHelper helper = key -> getReturnType(lookup.getEncodedValue(key, EncodedValue.class));
parseExpressions(expressions, nameInConditionValidator, info, createObjects, list, helper);
return new Parser(new org.codehaus.janino.Scanner(info, new StringReader(expressions.toString()))).
parseBlockStatements();
}
static void parseExpressions(StringBuilder expressions, NameValidator nameInConditionValidator,
String exceptionInfo, Set createObjects, List list,
ClassHelper classHelper) {
for (Statement statement : list) {
// avoid parsing the RHS value expression again as we just did it to get the maximum values in createClazz
if (statement.getKeyword() == Statement.Keyword.ELSE) {
if (!Helper.isEmpty(statement.getCondition()))
throw new IllegalArgumentException("condition must be empty but was " + statement.getCondition());
expressions.append("else {").append(statement.getOperation().build(statement.getValue())).append("; }\n");
} else if (statement.getKeyword() == Statement.Keyword.ELSEIF || statement.getKeyword() == Statement.Keyword.IF) {
ParseResult parseResult = ConditionalExpressionVisitor.parse(statement.getCondition(), nameInConditionValidator, classHelper);
if (!parseResult.ok)
throw new IllegalArgumentException(exceptionInfo + " invalid condition \"" + statement.getCondition() + "\"" +
(parseResult.invalidMessage == null ? "" : ": " + parseResult.invalidMessage));
createObjects.addAll(parseResult.guessedVariables);
if (statement.getKeyword() == Statement.Keyword.ELSEIF)
expressions.append("else ");
expressions.append("if (").append(parseResult.converted).append(") {").append(statement.getOperation().build(statement.getValue())).append(";}\n");
} else {
throw new IllegalArgumentException("The statement must be either 'if', 'else_if' or 'else'");
}
}
expressions.append("return value;\n");
}
/**
* Injects the already parsed expressions (converted to BlockStatement) via Janino's DeepCopier to the provided
* CompilationUnit cu (a class file).
*/
private static Java.CompilationUnit injectStatements(List priorityStatements,
List speedStatements,
Java.CompilationUnit cu) throws CompileException {
cu = new DeepCopier() {
boolean speedInjected = false;
boolean priorityInjected = false;
@Override
public Java.MethodDeclarator copyMethodDeclarator(Java.MethodDeclarator subject) throws CompileException {
if (subject.name.equals("getSpeed") && !speedStatements.isEmpty() && !speedInjected) {
speedInjected = true;
return injectStatements(subject, this, speedStatements);
} else if (subject.name.equals("getPriority") && !priorityStatements.isEmpty() && !priorityInjected) {
priorityInjected = true;
return injectStatements(subject, this, priorityStatements);
} else {
return super.copyMethodDeclarator(subject);
}
}
}.copyCompilationUnit(cu);
return cu;
}
private static Java.MethodDeclarator injectStatements(Java.MethodDeclarator subject, DeepCopier deepCopier,
List statements) {
try {
if (statements.isEmpty())
throw new IllegalArgumentException("Statements cannot be empty when copying method");
Java.MethodDeclarator methodDecl = new Java.MethodDeclarator(
new Location("m1", 1, 1),
subject.getDocComment(),
deepCopier.copyModifiers(subject.getModifiers()),
deepCopier.copyOptionalTypeParameters(subject.typeParameters),
deepCopier.copyType(subject.type),
subject.name,
deepCopier.copyFormalParameters(subject.formalParameters),
deepCopier.copyTypes(subject.thrownExceptions),
deepCopier.copyOptionalElementValue(subject.defaultValue),
deepCopier.copyOptionalStatements(statements)
);
statements.forEach(st -> st.setEnclosingScope(methodDecl));
return methodDecl;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private static SimpleCompiler createCompiler(long counter, Java.AbstractCompilationUnit cu) throws CompileException {
if (JANINO_DEBUG) {
try {
StringWriter sw = new StringWriter();
Unparser.unparse(cu, sw);
// System.out.println(sw.toString());
File dir = new File(SCRIPT_FILE_DIR);
File temporaryFile = new File(dir, "JaninoCustomWeightingHelperSubclass" + counter + ".java");
Reader reader = Readers.teeReader(
new StringReader(sw.toString()), // in
new FileWriter(temporaryFile), // out
true // closeWriterOnEoi
);
return new SimpleCompiler(temporaryFile.getAbsolutePath(), reader);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
} else {
SimpleCompiler compiler = new SimpleCompiler();
// compiler.setWarningHandler((handle, message, location) -> System.out.println(handle + ", " + message + ", " + location));
compiler.cook(cu);
return compiler;
}
}
}