diff --git a/compiler/mx.compiler/suite.py b/compiler/mx.compiler/suite.py index 44c8551c3b11..c528d0471775 100644 --- a/compiler/mx.compiler/suite.py +++ b/compiler/mx.compiler/suite.py @@ -2168,7 +2168,7 @@ "jdk.unsupported" # sun.misc.Unsafe ], "exports" : [ - "* to com.oracle.graal.graal_enterprise,org.graalvm.nativeimage.pointsto,org.graalvm.nativeimage.builder,org.graalvm.nativeimage.llvm,com.oracle.svm.svm_enterprise", + "* to com.oracle.graal.graal_enterprise,org.graalvm.nativeimage.pointsto,org.graalvm.nativeimage.builder,org.graalvm.nativeimage.llvm,com.oracle.svm.svm_enterprise,org.graalvm.nativeimage.base", "org.graalvm.compiler.core.common to jdk.internal.vm.compiler.management,org.graalvm.nativeimage.agent.tracing", "org.graalvm.compiler.debug to jdk.internal.vm.compiler.management,org.graalvm.nativeimage.objectfile", "org.graalvm.compiler.hotspot to jdk.internal.vm.compiler.management", diff --git a/substratevm/mx.substratevm/mx_substratevm.py b/substratevm/mx.substratevm/mx_substratevm.py index 2d468388df9e..061b9a99ad02 100644 --- a/substratevm/mx.substratevm/mx_substratevm.py +++ b/substratevm/mx.substratevm/mx_substratevm.py @@ -769,6 +769,7 @@ def native_image_context_run(func, func_args=None, config=None, build_if_missing 'substratevm:SVM', 'substratevm:OBJECTFILE', 'substratevm:POINTSTO', + 'substratevm:NATIVE_IMAGE_BASE', ], support_distributions=['substratevm:SVM_GRAALVM_SUPPORT'], stability="earlyadopter", diff --git a/substratevm/mx.substratevm/suite.py b/substratevm/mx.substratevm/suite.py index c1b7e79a15bf..f8b4f7afa75e 100644 --- a/substratevm/mx.substratevm/suite.py +++ b/substratevm/mx.substratevm/suite.py @@ -147,6 +147,19 @@ "checkstyle": "com.oracle.svm.core", "workingSets": "SVM", }, + "com.oracle.svm.common": { + "subDir": "src", + "sourceDirs": ["src"], + "dependencies": [ + "com.oracle.svm.util" + ], + "javaCompliance": "8+", + "annotationProcessors": [ + "compiler:GRAAL_PROCESSOR", + ], + "checkstyle": "com.oracle.svm.core", + "workingSets": "SVM", + }, "com.oracle.svm.util.jdk11": { "subDir": "src", "sourceDirs": ["src"], @@ -174,7 +187,7 @@ "headers", ], "dependencies": [ - "com.oracle.svm.util", + "com.oracle.svm.common", ], "javaCompliance": "8+", "checkstyleVersion" : "8.36.1", @@ -387,7 +400,7 @@ "subDir": "src", "sourceDirs": ["src"], "dependencies": [ - "com.oracle.svm.util", + "com.oracle.svm.common", ], "checkstyle": "com.oracle.graal.pointsto", "javaCompliance": "8+", @@ -1106,6 +1119,7 @@ "OBJECTFILE", "POINTSTO", "compiler:GRAAL", + "NATIVE_IMAGE_BASE", ], "moduleInfo" : { "name" : "org.graalvm.nativeimage.builder", @@ -1119,7 +1133,7 @@ "com.oracle.svm.hosted to java.base", "com.oracle.svm.hosted.agent to java.instrument", "com.oracle.svm.truffle.api to org.graalvm.truffle", - "* to jdk.internal.vm.compiler,org.graalvm.nativeimage.driver,org.graalvm.nativeimage.configure,org.graalvm.nativeimage.librarysupport,org.graalvm.nativeimage.junitsupport,org.graalvm.nativeimage.llvm,org.graalvm.nativeimage.agent.jvmtibase,org.graalvm.nativeimage.agent.tracing,org.graalvm.nativeimage.agent.diagnostics,com.oracle.svm.svm_enterprise", + "* to org.graalvm.nativeimage.base,jdk.internal.vm.compiler,org.graalvm.nativeimage.driver,org.graalvm.nativeimage.configure,org.graalvm.nativeimage.librarysupport,org.graalvm.nativeimage.junitsupport,org.graalvm.nativeimage.llvm,org.graalvm.nativeimage.agent.jvmtibase,org.graalvm.nativeimage.agent.tracing,org.graalvm.nativeimage.agent.diagnostics,com.oracle.svm.svm_enterprise", ], "opens" : [ "com.oracle.svm.core to jdk.internal.vm.compiler", @@ -1254,6 +1268,9 @@ "static junit", "static hamcrest", ], + "exports" : [ + "* to org.graalvm.nativeimage.base,org.graalvm.nativeimage.pointsto,org.graalvm.nativeimage.builder", + ], }, }, @@ -1421,6 +1438,26 @@ }, }, + "NATIVE_IMAGE_BASE": { + "subDir": "src", + "description" : "Native Image base that can be shared by native image building and pointsto.", + "dependencies": [ + "com.oracle.svm.common", + "com.oracle.svm.util", + ], + "distDependencies": [ + "compiler:GRAAL", + ], + "exclude": [ + ], + "moduleInfo" : { + "name" : "org.graalvm.nativeimage.base", + "exports" : [ + "com.oracle.svm.util", + "com.oracle.svm.common.option", + ], + } + }, "POINTSTO": { "subDir": "src", @@ -1431,13 +1468,13 @@ ], "distDependencies": [ "compiler:GRAAL", + "NATIVE_IMAGE_BASE", ], "exclude": [ ], "moduleInfo" : { "name" : "org.graalvm.nativeimage.pointsto", "exports" : [ - "com.oracle.svm.util", "com.oracle.graal.pointsto", "com.oracle.graal.pointsto.api", "com.oracle.graal.pointsto.reports", @@ -1614,7 +1651,7 @@ "moduleInfo" : { "name" : "org.graalvm.nativeimage.llvm", "exports" : [ - "* to org.graalvm.nativeimage.builder", + "* to org.graalvm.nativeimage.builder,org.graalvm.nativeimage.base", ], }, "maven" : False, diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/util/AnalysisError.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/util/AnalysisError.java index b9f58533b894..8d59e33cd5a3 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/util/AnalysisError.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/util/AnalysisError.java @@ -125,6 +125,14 @@ private static String message(PointsToAnalysis bb, TypeFlow objectFlow, Bytec } } + public static class InterruptAnalysis extends AnalysisError { + private static final long serialVersionUID = 7126612141948542452L; + + InterruptAnalysis(String msg) { + super(msg); + } + } + public static TypeNotFoundError typeNotFound(ResolvedJavaType type) { throw new TypeNotFoundError(type); } @@ -166,4 +174,8 @@ public static void guarantee(boolean condition, String format, Object... args) { // Checkstyle: resume } } + + public static RuntimeException interruptAnalysis(String msg) { + throw new InterruptAnalysis(msg); + } } diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/util/PointsToOptionParser.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/util/PointsToOptionParser.java new file mode 100644 index 000000000000..b2a93a85768d --- /dev/null +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/util/PointsToOptionParser.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2021, Alibaba Group Holding Limited. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.graal.pointsto.util; + +import com.oracle.svm.common.option.CommonOptionParser; +import com.oracle.svm.common.option.CommonOptionParser.BooleanOptionFormat; +import com.oracle.svm.common.option.CommonOptionParser.OptionParseResult; +import com.oracle.svm.common.option.UnsupportedOptionClassException; +import org.graalvm.collections.EconomicMap; +import org.graalvm.compiler.options.OptionDescriptor; +import org.graalvm.compiler.options.OptionDescriptors; +import org.graalvm.compiler.options.OptionKey; +import org.graalvm.compiler.options.OptionValues; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.function.Predicate; + +import static com.oracle.svm.common.option.CommonOptionParser.BooleanOptionFormat.PLUS_MINUS; + +public final class PointsToOptionParser { + + private static PointsToOptionParser instance = new PointsToOptionParser(); + + private OptionValues optionValues = null; + private EconomicMap, Object> analysisValues = OptionValues.newOptionMap(); + private EconomicMap allAnalysisOptions = EconomicMap.create(); + + public static PointsToOptionParser getInstance() { + return instance; + } + + @SuppressWarnings("unchecked") + private PointsToOptionParser() { + ClassLoader appClassLoader = PointsToOptionParser.class.getClassLoader(); + CommonOptionParser.collectOptions(ServiceLoader.load(OptionDescriptors.class, appClassLoader), descriptor -> { + String name = descriptor.getName(); + if (descriptor.getOptionKey() instanceof OptionKey) { + OptionDescriptor existing = allAnalysisOptions.put(name, descriptor); + if (existing != null) { + AnalysisError.shouldNotReachHere("Option name \"" + name + "\" has multiple definitions: " + existing.getLocation() + " and " + descriptor.getLocation()); + } + } + }); + } + + public void setOptionValue(OptionKey optionKey, Object value) { + if (optionValues == null) { + parse(new String[0]); + } + optionKey.update((EconomicMap, Object>) optionValues.getMap(), value); + } + + public OptionValues parse(String[] args) { + List remainingArgs = new ArrayList<>(); + Set errors = new HashSet<>(); + for (String arg : args) { + boolean isAnalysisOption = false; + isAnalysisOption |= parseOption(CommonOptionParser.HOSTED_OPTION_PREFIX, allAnalysisOptions, analysisValues, PLUS_MINUS, errors, arg, System.out); + if (!isAnalysisOption) { + remainingArgs.add(arg); + } + } + optionValues = new OptionValues(analysisValues); + if (!remainingArgs.isEmpty()) { + AnalysisError.interruptAnalysis(String.format("Unknown options: %s", Arrays.toString(remainingArgs.toArray(new String[0])))); + } + if (!errors.isEmpty()) { + StringBuilder errMsg = new StringBuilder("Option format error:\n"); + for (String err : errors) { + errMsg.append(err).append("\n"); + } + AnalysisError.interruptAnalysis(errMsg.toString()); + } + return optionValues; + } + + private static boolean parseOption(String optionPrefix, EconomicMap options, EconomicMap, Object> valuesMap, + BooleanOptionFormat booleanOptionFormat, Set errors, String arg, PrintStream out) { + if (!arg.startsWith(optionPrefix)) { + return false; + } + try { + Predicate> optionKeyPredicate = optionKey -> { + Class clazz = optionKey.getClass(); + // All classes from com.oracle.graal.pointsto.api.PointstoOptions are taken as non-hosted options. + if (clazz.getName().startsWith("com.oracle.graal.pointsto.api.PointstoOptions")) { + return false; + } + if (!clazz.equals(OptionKey.class) && OptionKey.class.isAssignableFrom(clazz)) { + return true; + } else { + return false; + } + }; + OptionParseResult optionParseResult = CommonOptionParser.parseOption(options, optionKeyPredicate, arg.substring(optionPrefix.length()), valuesMap, + optionPrefix, booleanOptionFormat); + if (optionParseResult.printFlags() || optionParseResult.printFlagsWithExtraHelp()) { + CommonOptionParser.printFlags(d -> optionParseResult.matchesFlags(d, () -> true), options, optionPrefix, out, optionParseResult.printFlagsWithExtraHelp()); + System.out.println("Abort analysis due to print flags are requested"); + System.exit(1); + } + if (!optionParseResult.isValid()) { + errors.add(optionParseResult.getError()); + } + } catch (UnsupportedOptionClassException e) { + AnalysisError.shouldNotReachHere(e); + } + return true; + } +} diff --git a/substratevm/src/com.oracle.svm.common/src/com/oracle/svm/common/option/CommonOptionParser.java b/substratevm/src/com.oracle.svm.common/src/com/oracle/svm/common/option/CommonOptionParser.java new file mode 100644 index 000000000000..823a67bee2c9 --- /dev/null +++ b/substratevm/src/com.oracle.svm.common/src/com/oracle/svm/common/option/CommonOptionParser.java @@ -0,0 +1,522 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2021, Alibaba Group Holding Limited. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.common.option; + +// Checkstyle: allow reflection + +import com.oracle.svm.util.ClassUtil; +import com.oracle.svm.util.StringUtil; +import org.graalvm.collections.EconomicMap; +import org.graalvm.compiler.options.OptionDescriptor; +import org.graalvm.compiler.options.OptionDescriptors; +import org.graalvm.compiler.options.OptionKey; +import org.graalvm.compiler.options.OptionType; +import org.graalvm.compiler.options.OptionsParser; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public class CommonOptionParser { + public static final String HOSTED_OPTION_PREFIX = "-H:"; + public static final String RUNTIME_OPTION_PREFIX = "-R:"; + + public static final int PRINT_OPTION_INDENTATION = 2; + public static final int PRINT_OPTION_WIDTH = 45; + public static final int PRINT_OPTION_WRAP_WIDTH = 120; + + /** + * The result of {@link CommonOptionParser#parseOption}. + */ + public static final class OptionParseResult { + private final EnumSet printFlags; + private final Set optionNameFilter; + private final String error; + private final OptionKey optionKey; + private static final String EXTRA_HELP_OPTIONS_WILDCARD = "*"; + + OptionParseResult(EnumSet printFlags, String error, Set optionNameFilter, OptionKey optionKey) { + this.printFlags = printFlags; + this.error = error; + this.optionNameFilter = optionNameFilter; + this.optionKey = optionKey; + } + + private OptionParseResult(EnumSet printFlags, String error, OptionKey optionKey) { + this(printFlags, error, new HashSet<>(), optionKey); + } + + static OptionParseResult error(String message) { + return new OptionParseResult(EnumSet.noneOf(OptionType.class), message, null); + } + + static OptionParseResult correct(OptionKey optionKey) { + return new OptionParseResult(EnumSet.noneOf(OptionType.class), null, optionKey); + } + + static OptionParseResult printFlags(EnumSet selectedOptionTypes) { + return new OptionParseResult(selectedOptionTypes, null, null); + } + + static OptionParseResult printFlagsWithExtraHelp(Set optionNameFilter) { + Set optionNames = optionNameFilter; + if (optionNames.contains(EXTRA_HELP_OPTIONS_WILDCARD)) { + optionNames = new HashSet<>(); + optionNames.add(EXTRA_HELP_OPTIONS_WILDCARD); + } + return new OptionParseResult(EnumSet.noneOf(OptionType.class), null, optionNames, null); + } + + public boolean printFlags() { + return !printFlags.isEmpty(); + } + + public boolean printFlagsWithExtraHelp() { + return !optionNameFilter.isEmpty(); + } + + public boolean isValid() { + boolean result = optionKey != null; + assert result == (printFlags.isEmpty() && optionNameFilter.isEmpty() && error == null); + return result; + } + + public String getError() { + return error; + } + + public OptionKey getOptionKey() { + return optionKey; + } + + public boolean matchesFlags(OptionDescriptor d, BooleanSupplier svmOption) { + if (!printFlags.isEmpty()) { + boolean showAll = printFlags.equals(EnumSet.allOf(OptionType.class)); + return showAll || svmOption.getAsBoolean() && printFlags.contains(d.getOptionType()); + } + if (!optionNameFilter.isEmpty()) { + if (optionNameFilter.contains(EXTRA_HELP_OPTIONS_WILDCARD) && !d.getExtraHelp().isEmpty()) { + return true; + } + return optionNameFilter.contains(d.getName()); + } + return false; + } + } + + /** + * Constants denoting supported boolean option formats. + */ + public enum BooleanOptionFormat { + NAME_VALUE("="), + PLUS_MINUS("+/-"); + + BooleanOptionFormat(String help) { + this.help = help; + } + + private final String help; + + @Override + public String toString() { + return help; + } + } + + public static void collectOptions(ServiceLoader optionDescriptors, Consumer optionDescriptorConsumer) { + for (OptionDescriptors optionDescriptor : optionDescriptors) { + for (OptionDescriptor descriptor : optionDescriptor) { + optionDescriptorConsumer.accept(descriptor); + } + } + } + + public static OptionParseResult parseOption(EconomicMap options, Predicate> isHosted, String option, EconomicMap, Object> valuesMap, + String optionPrefix, BooleanOptionFormat booleanOptionFormat) throws UnsupportedOptionClassException { + if (option.length() == 0) { + return OptionParseResult.error("Option name must be specified"); + } + + String optionName; + Object value = null; + String valueString = null; + + char first = option.charAt(0); + int eqIndex = option.indexOf('='); + if (first == '+' || first == '-') { + if (eqIndex != -1) { + return OptionParseResult.error("Cannot mix +/- with = format: '" + optionPrefix + option + "'"); + } + optionName = option.substring(1); + if (booleanOptionFormat == BooleanOptionFormat.NAME_VALUE) { + return OptionParseResult.error("Option " + LocatableOption.from(optionName) + " must use = format, not +/- prefix"); + } + value = (first == '+'); + } else { + if (eqIndex == -1) { + optionName = option; + valueString = null; + } else { + optionName = option.substring(0, eqIndex); + valueString = option.substring(eqIndex + 1); + } + } + + LocatableOption current = LocatableOption.from(optionName); + OptionDescriptor desc = options.get(current.name); + if (desc == null && value != null) { + if (eqIndex != -1) { + optionName = option.substring(1, eqIndex); + current = LocatableOption.from(optionName); + desc = options.get(current.name); + } + } + + optionName = current.name; + + if (desc == null) { + List matches = new ArrayList<>(); + OptionsParser.collectFuzzyMatches(options.getValues(), optionName, matches); + StringBuilder msg = new StringBuilder("Could not find option ").append(current); + if (!matches.isEmpty()) { + msg.append(". Did you mean one of these:"); + for (OptionDescriptor match : matches) { + msg.append(' ').append(match.getName()); + } + } + msg.append(". Use ").append(optionPrefix).append(CommonOptions.PrintFlags.getName()).append("= to list all available options."); + return OptionParseResult.error(msg.toString()); + } + + OptionKey optionKey = desc.getOptionKey(); + boolean hostedOption = isHosted.test(optionKey); + Class optionValueType = getMultiOptionValueElementType(optionKey); + Class optionType = hostedOption && optionValueType != null ? optionValueType : desc.getOptionValueType(); + + if (value == null) { + if (optionType == Boolean.class && booleanOptionFormat == BooleanOptionFormat.PLUS_MINUS) { + return OptionParseResult.error("Boolean option " + current + " must have +/- prefix"); + } + if (valueString == null) { + return OptionParseResult.error("Missing value for option " + current); + } + try { + value = parseValue(optionType, current, valueString); + if (value instanceof OptionParseResult) { + return (OptionParseResult) value; + } + } catch (NumberFormatException ex) { + return OptionParseResult.error("Invalid value for option " + current + ": '" + valueString + "' is not a valid number"); + } + } else { + if (optionType != Boolean.class) { + return OptionParseResult.error("Non-boolean option " + current + " can not use +/- prefix. Use '" + current.name + "=' format"); + } + } + + optionKey.update(valuesMap, hostedOption ? LocatableOption.value(value, current.origin) : value); + + if (CommonOptions.PrintFlags.getName().equals(optionName)) { + String optionValue = (String) value; + EnumSet selectedOptionTypes; + if (optionValue.isEmpty()) { + selectedOptionTypes = EnumSet.allOf(OptionType.class); + } else { + selectedOptionTypes = EnumSet.noneOf(OptionType.class); + String enumString = null; + try { + String[] enumStrings = StringUtil.split(optionValue, ","); + + for (String string : enumStrings) { + enumString = string; + selectedOptionTypes.add(OptionType.valueOf(enumString)); + } + } catch (IllegalArgumentException e) { + StringBuilder sb = new StringBuilder(); + boolean firstValue = true; + for (OptionType ot : OptionType.values()) { + if (firstValue) { + firstValue = false; + } else { + sb.append(", "); + } + sb.append(ot.name()); + } + String possibleValues = sb.toString(); + return OptionParseResult.error("Invalid value for option " + current + ". '" + enumString + "' is not one of: " + possibleValues); + } + } + return OptionParseResult.printFlags(selectedOptionTypes); + } + + if (CommonOptions.PrintFlagsWithExtraHelp.getName().equals(optionName)) { + String optionValue = (String) value; + String[] optionNames = StringUtil.split(optionValue, ","); + HashSet selectedOptionNames = new HashSet<>(Arrays.asList(optionNames)); + return OptionParseResult.printFlagsWithExtraHelp(selectedOptionNames); + } + return OptionParseResult.correct(optionKey); + } + + @SuppressWarnings("unchecked") + static Object parseValue(Class optionType, LocatableOption option, String valueString) throws NumberFormatException, UnsupportedOptionClassException { + Object value; + if (optionType == Integer.class) { + long longValue = parseLong(valueString); + if ((int) longValue != longValue) { + return OptionParseResult.error("Wrong value for option " + option + ": '" + valueString + "' is not a valid number"); + } + value = (int) longValue; + } else if (optionType == Long.class) { + value = parseLong(valueString); + } else if (optionType == String.class) { + value = valueString; + } else if (optionType == Double.class) { + value = parseDouble(valueString); + } else if (optionType == Boolean.class) { + if (valueString.equals("true")) { + value = true; + } else if (valueString.equals("false")) { + value = false; + } else { + return OptionParseResult.error("Boolean option " + option + " must have value 'true' or 'false'"); + } + } else if (optionType.isEnum()) { + value = Enum.valueOf(optionType.asSubclass(Enum.class), valueString); + } else { + throw new UnsupportedOptionClassException(option + " uses unsupported option value class: " + ClassUtil.getUnqualifiedName(optionType)); + } + return value; + } + + private static Class getMultiOptionValueElementType(OptionKey optionKey) { + Object defaultValue = optionKey.getDefaultValue(); + if (defaultValue instanceof MultiOptionValue) { + return ((MultiOptionValue) defaultValue).getValueType(); + } + return null; + } + + public static long parseLong(String v) { + String valueString = v.trim().toLowerCase(); + long scale = 1; + if (valueString.endsWith("k")) { + scale = 1024L; + } else if (valueString.endsWith("m")) { + scale = 1024L * 1024L; + } else if (valueString.endsWith("g")) { + scale = 1024L * 1024L * 1024L; + } else if (valueString.endsWith("t")) { + scale = 1024L * 1024L * 1024L * 1024L; + } + + if (scale != 1) { + /* Remove trailing scale character. */ + valueString = valueString.substring(0, valueString.length() - 1); + } + + return Long.parseLong(valueString) * scale; + } + + /** + * Parses the provide string to a double number, avoiding the JDK dependencies (which pull in a + * lot of classes, including the regular expression library). Only simple numbers are supported, + * without fancy exponent styles. + */ + public static double parseDouble(String v) { + String valueString = v.trim(); + + int dotPos = valueString.indexOf('.'); + if (dotPos == -1) { + return parseLong(valueString); + } + + String beforeDot = valueString.substring(0, dotPos); + String afterDot = valueString.substring(dotPos + 1); + + double sign = 1; + if (beforeDot.startsWith("-")) { + sign = -1; + beforeDot = beforeDot.substring(1); + } else if (beforeDot.startsWith("+")) { + beforeDot = beforeDot.substring(1); + } + + if (beforeDot.startsWith("-") || beforeDot.startsWith("+") || afterDot.startsWith("-") || afterDot.startsWith("+") || + (beforeDot.length() == 0 && afterDot.length() == 0)) { + throw new NumberFormatException(v); + } + + double integral = 0; + if (beforeDot.length() > 0) { + integral = Long.parseLong(beforeDot); + } + + double fraction = 0; + if (afterDot.length() > 0) { + fraction = Long.parseLong(afterDot) * Math.pow(10, -afterDot.length()); + } + + return sign * (integral + fraction); + } + + private static String spaces(int length) { + return new String(new char[length]).replace('\0', ' '); + } + + private static String wrap(String s, int width) { + StringBuilder sb = new StringBuilder(s); + int cursor = 0; + while (cursor + width < sb.length()) { + int i = sb.lastIndexOf(" ", cursor + width); + if (i == -1 || i < cursor) { + i = sb.indexOf(" ", cursor + width); + } + if (i != -1) { + sb.replace(i, i + 1, System.lineSeparator()); + cursor = i; + } else { + break; + } + } + return sb.toString(); + } + + private static void printOption(PrintStream out, String option, String description, int wrap) { + printOption(out::println, option, description, PRINT_OPTION_INDENTATION, PRINT_OPTION_WIDTH, wrap); + } + + public static void printOption(Consumer println, String option, String description, int indentation, int optionWidth, int wrapWidth) { + String indent = spaces(indentation); + String desc = description != null ? description : ""; + desc = wrapWidth > 0 ? wrap(desc, wrapWidth) : desc; + String nl = System.lineSeparator(); + String[] descLines = StringUtil.split(desc, nl); + if (option.length() >= optionWidth && description != null) { + println.accept(indent + option + nl + indent + spaces(optionWidth) + descLines[0]); + } else { + println.accept(indent + option + spaces(optionWidth - option.length()) + descLines[0]); + } + for (int i = 1; i < descLines.length; i++) { + println.accept(indent + spaces(optionWidth) + descLines[i]); + } + } + + public static void printFlags(Predicate filter, EconomicMap options, String prefix, PrintStream out, boolean verbose) { + List sortedDescriptors = new ArrayList<>(); + for (OptionDescriptor option : options.getValues()) { + if (filter.test(option)) { + sortedDescriptors.add(option); + } + } + sortedDescriptors.sort(Comparator.comparing(OptionDescriptor::getName)); + + for (OptionDescriptor descriptor : sortedDescriptors) { + String helpMsg = verbose && !descriptor.getExtraHelp().isEmpty() ? "" : descriptor.getHelp(); + int helpLen = helpMsg.length(); + if (helpLen > 0 && helpMsg.charAt(helpLen - 1) != '.') { + helpMsg += '.'; + } + boolean stringifiedArrayValue = false; + Object defaultValue = descriptor.getOptionKey().getDefaultValue(); + if (defaultValue != null && defaultValue.getClass().isArray()) { + Object[] defaultValues = (Object[]) defaultValue; + if (defaultValues.length == 1) { + defaultValue = defaultValues[0]; + } else { + List stringList = new ArrayList<>(); + String optionPrefix = prefix + descriptor.getName() + "="; + for (Object rawValue : defaultValues) { + String value; + if (rawValue instanceof String) { + value = '"' + String.valueOf(rawValue) + '"'; + } else { + value = String.valueOf(rawValue); + } + stringList.add(optionPrefix + value); + } + if (helpLen != 0) { + helpMsg += ' '; + } + helpMsg += "Default: "; + if (stringList.isEmpty()) { + helpMsg += "None"; + } else { + helpMsg += String.join(" ", stringList); + } + stringifiedArrayValue = true; + } + } + String verboseHelp = ""; + if (verbose) { + verboseHelp = System.lineSeparator() + descriptor.getHelp() + System.lineSeparator() + String.join(System.lineSeparator(), descriptor.getExtraHelp()); + } else if (!descriptor.getExtraHelp().isEmpty()) { + verboseHelp = " [Extra help available]"; + } + int wrapWidth = verbose ? 0 : PRINT_OPTION_WRAP_WIDTH; + if (descriptor.getOptionValueType() == Boolean.class) { + Boolean val = (Boolean) defaultValue; + if (helpLen != 0) { + helpMsg += ' '; + } + if (val != null) { + if (val) { + helpMsg += "Default: + (enabled)."; + } else { + helpMsg += "Default: - (disabled)."; + } + } + printOption(out, prefix + "\u00b1" + descriptor.getName(), helpMsg + verboseHelp, wrapWidth); + } else { + if (defaultValue == null) { + if (helpLen != 0) { + helpMsg += ' '; + } + helpMsg += "Default: None"; + } + helpMsg += verboseHelp; + if (stringifiedArrayValue || defaultValue == null) { + printOption(out, prefix + descriptor.getName() + "=...", helpMsg, wrapWidth); + } else { + if (defaultValue instanceof String) { + defaultValue = '"' + String.valueOf(defaultValue) + '"'; + } + printOption(out, prefix + descriptor.getName() + "=" + defaultValue, helpMsg, wrapWidth); + } + } + } + } +} diff --git a/substratevm/src/com.oracle.svm.common/src/com/oracle/svm/common/option/CommonOptions.java b/substratevm/src/com.oracle.svm.common/src/com/oracle/svm/common/option/CommonOptions.java new file mode 100644 index 000000000000..6b2f9e88b5d6 --- /dev/null +++ b/substratevm/src/com.oracle.svm.common/src/com/oracle/svm/common/option/CommonOptions.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2021, Alibaba Group Holding Limited. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.common.option; + +import org.graalvm.compiler.options.Option; +import org.graalvm.compiler.options.OptionKey; + +public class CommonOptions { + + @Option(help = "Show available options based on comma-separated option-types (allowed categories: User, Expert, Debug).")// + public static final OptionKey PrintFlags = new OptionKey<>(null); + + @Option(help = "Print extra help, if available, based on comma-separated option names. Pass * to show all options that contain extra help.")// + public static final OptionKey PrintFlagsWithExtraHelp = new OptionKey<>(null); +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/LocatableOption.java b/substratevm/src/com.oracle.svm.common/src/com/oracle/svm/common/option/LocatableOption.java similarity index 96% rename from substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/LocatableOption.java rename to substratevm/src/com.oracle.svm.common/src/com/oracle/svm/common/option/LocatableOption.java index d675521de43d..62927c9a3a31 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/LocatableOption.java +++ b/substratevm/src/com.oracle.svm.common/src/com/oracle/svm/common/option/LocatableOption.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2021, Alibaba Group Holding Limited. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -22,7 +23,7 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ -package com.oracle.svm.core.option; +package com.oracle.svm.common.option; public final class LocatableOption { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/MultiOptionValue.java b/substratevm/src/com.oracle.svm.common/src/com/oracle/svm/common/option/MultiOptionValue.java similarity index 84% rename from substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/MultiOptionValue.java rename to substratevm/src/com.oracle.svm.common/src/com/oracle/svm/common/option/MultiOptionValue.java index ad6207f9036e..0f678439903c 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/MultiOptionValue.java +++ b/substratevm/src/com.oracle.svm.common/src/com/oracle/svm/common/option/MultiOptionValue.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2021, Alibaba Group Holding Limited. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -22,11 +23,11 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ -package com.oracle.svm.core.option; +package com.oracle.svm.common.option; import java.util.List; -interface MultiOptionValue { +public interface MultiOptionValue { Class getValueType(); @@ -34,7 +35,7 @@ interface MultiOptionValue { * @return a list of option values, one for each place where the option is used * @implSpec Note that it DOES NOT perform any splitting of string values based on a delimiter. * If you want to perform this split, use a utility - * {@link OptionUtils#flatten(String, LocatableMultiOptionValue.Strings)} + * {@link com.oracle.svm.core.option.OptionUtils#flatten(String, com.oracle.svm.core.option.LocatableMultiOptionValue.Strings)} */ List values(); diff --git a/substratevm/src/com.oracle.svm.common/src/com/oracle/svm/common/option/OptionParsingException.java b/substratevm/src/com.oracle.svm.common/src/com/oracle/svm/common/option/OptionParsingException.java new file mode 100644 index 000000000000..87e2d281bdd8 --- /dev/null +++ b/substratevm/src/com.oracle.svm.common/src/com/oracle/svm/common/option/OptionParsingException.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2021, Alibaba Group Holding Limited. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.common.option; + +public class OptionParsingException extends Exception { + private static final long serialVersionUID = 1413275434484971842L; + + public OptionParsingException(Exception e) { + super(e); + } +} diff --git a/substratevm/src/com.oracle.svm.common/src/com/oracle/svm/common/option/UnsupportedOptionClassException.java b/substratevm/src/com.oracle.svm.common/src/com/oracle/svm/common/option/UnsupportedOptionClassException.java new file mode 100644 index 000000000000..c92195df9443 --- /dev/null +++ b/substratevm/src/com.oracle.svm.common/src/com/oracle/svm/common/option/UnsupportedOptionClassException.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2021, Alibaba Group Holding Limited. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.common.option; + +public class UnsupportedOptionClassException extends Exception { + private static final long serialVersionUID = -3105370072461246590L; + + public UnsupportedOptionClassException(String msg) { + super(msg); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java index 399eb506bfe0..0ceb845846fa 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java @@ -125,12 +125,6 @@ protected void onValueUpdate(EconomicMap, Object> values, Boolean o private static ValueUpdateHandler optimizeValueUpdateHandler; private static ValueUpdateHandler debugInfoValueUpdateHandler = SubstrateOptions::defaultDebugInfoValueUpdateHandler; - @Option(help = "Show available options based on comma-separated option-types (allowed categories: User, Expert, Debug).")// - public static final OptionKey PrintFlags = new OptionKey<>(null); - - @Option(help = "Print extra help, if available, based on comma-separated option names. Pass * to show all options that contain extra help.")// - public static final OptionKey PrintFlagsWithExtraHelp = new OptionKey<>(null); - @Option(help = "Control native-image code optimizations: 0 - no optimizations, 1 - basic optimizations, 2 - aggressive optimizations.", type = OptionType.User)// public static final HostedOptionKey Optimize = new HostedOptionKey(2) { @Override diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateUtil.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateUtil.java index 0cb5e35fdeb1..fc52b1c7a2d7 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateUtil.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateUtil.java @@ -34,11 +34,11 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.lang.reflect.Method; -import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.oracle.svm.util.StringUtil; import org.graalvm.compiler.graph.Node.NodeIntrinsic; import org.graalvm.compiler.java.LambdaUtils; import org.graalvm.compiler.nodes.BreakpointNode; @@ -270,31 +270,7 @@ public static String[] split(String value, String separator) { * regular expression. This avoids making regular expression code reachable. */ public static String[] split(String value, String separator, int limit) { - int offset = 0; - int next; - ArrayList list = null; - while ((next = value.indexOf(separator, offset)) != -1) { - if (list == null) { - list = new ArrayList<>(); - } - boolean limited = limit > 0; - if (!limited || list.size() < limit - 1) { - list.add(value.substring(offset, next)); - offset = next + separator.length(); - } else { - break; - } - } - - if (offset == 0) { - /* No match found. */ - return new String[]{value}; - } - - /* Add remaining segment. */ - list.add(value.substring(offset)); - - return list.toArray(new String[list.size()]); + return StringUtil.split(value, separator, limit); } public static String toHex(byte[] data) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/HostedOptionKey.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/HostedOptionKey.java index 70c23993eec9..4816367709d2 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/HostedOptionKey.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/HostedOptionKey.java @@ -24,6 +24,8 @@ */ package com.oracle.svm.core.option; +import com.oracle.svm.common.option.LocatableOption; +import com.oracle.svm.common.option.MultiOptionValue; import org.graalvm.collections.EconomicMap; import org.graalvm.compiler.api.replacements.Fold; import org.graalvm.compiler.options.Option; diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/LocatableMultiOptionValue.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/LocatableMultiOptionValue.java index bf43136b21d2..176087b6115f 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/LocatableMultiOptionValue.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/LocatableMultiOptionValue.java @@ -29,6 +29,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import com.oracle.svm.common.option.LocatableOption; +import com.oracle.svm.common.option.MultiOptionValue; import org.graalvm.collections.Pair; import com.oracle.svm.core.util.VMError; diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/RuntimeOptionParser.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/RuntimeOptionParser.java index ff5a104b6fe2..9dc6007070c4 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/RuntimeOptionParser.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/RuntimeOptionParser.java @@ -41,8 +41,8 @@ import com.oracle.svm.core.SubstrateOptions; import com.oracle.svm.core.log.Log; -import com.oracle.svm.core.option.SubstrateOptionsParser.BooleanOptionFormat; -import com.oracle.svm.core.option.SubstrateOptionsParser.OptionParseResult; +import com.oracle.svm.common.option.CommonOptionParser.BooleanOptionFormat; +import com.oracle.svm.common.option.CommonOptionParser.OptionParseResult; import com.oracle.svm.core.properties.RuntimePropertyParser; import com.oracle.svm.core.util.ImageHeapMap; @@ -163,7 +163,8 @@ private void parseOptionAtRuntime(String arg, String optionPrefix, BooleanOption Predicate> isHosted = optionKey -> false; OptionParseResult parseResult = SubstrateOptionsParser.parseOption(options, isHosted, arg.substring(optionPrefix.length()), values, optionPrefix, booleanOptionFormat); if (parseResult.printFlags() || parseResult.printFlagsWithExtraHelp()) { - SubstrateOptionsParser.printFlags(parseResult::matchesFlagsRuntime, options, optionPrefix, Log.logStream(), parseResult.printFlagsWithExtraHelp()); + SubstrateOptionsParser.printFlags(d -> parseResult.matchesFlags(d, () -> d.getOptionKey() instanceof RuntimeOptionKey), + options, optionPrefix, Log.logStream(), parseResult.printFlagsWithExtraHelp()); System.exit(0); } if (!parseResult.isValid()) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/SubstrateOptionsParser.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/SubstrateOptionsParser.java index f049bd43c7e9..81436ccd784c 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/SubstrateOptionsParser.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/SubstrateOptionsParser.java @@ -26,31 +26,28 @@ // Checkstyle: allow reflection +import com.oracle.svm.core.util.InterruptImageBuilding; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.common.option.CommonOptionParser; +import com.oracle.svm.common.option.CommonOptionParser.BooleanOptionFormat; +import com.oracle.svm.common.option.CommonOptionParser.OptionParseResult; +import com.oracle.svm.common.option.UnsupportedOptionClassException; +import org.graalvm.collections.EconomicMap; +import org.graalvm.compiler.options.OptionDescriptor; +import org.graalvm.compiler.options.OptionDescriptors; +import org.graalvm.compiler.options.OptionKey; +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; + import java.io.PrintStream; import java.lang.reflect.Field; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Comparator; -import java.util.EnumSet; -import java.util.HashSet; -import java.util.List; +import java.util.ServiceLoader; import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; -import org.graalvm.collections.EconomicMap; -import org.graalvm.compiler.options.OptionDescriptor; -import org.graalvm.compiler.options.OptionKey; -import org.graalvm.compiler.options.OptionType; -import org.graalvm.compiler.options.OptionsParser; -import org.graalvm.nativeimage.Platform; -import org.graalvm.nativeimage.Platforms; - -import com.oracle.svm.core.SubstrateOptions; -import com.oracle.svm.core.SubstrateUtil; -import com.oracle.svm.core.util.InterruptImageBuilding; -import com.oracle.svm.core.util.VMError; -import com.oracle.svm.util.ClassUtil; +import static com.oracle.svm.core.util.VMError.shouldNotReachHere; /** * This class contains methods for parsing options and matching them against @@ -58,283 +55,17 @@ */ public class SubstrateOptionsParser { - public static final String HOSTED_OPTION_PREFIX = "-H:"; - public static final String RUNTIME_OPTION_PREFIX = "-R:"; - public static final int PRINT_OPTION_INDENTATION = 2; - public static final int PRINT_OPTION_WIDTH = 45; - public static final int PRINT_OPTION_WRAP_WIDTH = 120; - - /** - * The result of {@link SubstrateOptionsParser#parseOption}. - */ - static final class OptionParseResult { - private final EnumSet printFlags; - private final Set optionNameFilter; - private final String error; - private final OptionKey optionKey; - private static final String EXTRA_HELP_OPTIONS_WILDCARD = "*"; - - private OptionParseResult(EnumSet printFlags, String error, Set optionNameFilter, OptionKey optionKey) { - this.printFlags = printFlags; - this.error = error; - this.optionNameFilter = optionNameFilter; - this.optionKey = optionKey; - } - - private OptionParseResult(EnumSet printFlags, String error, OptionKey optionKey) { - this(printFlags, error, new HashSet<>(), optionKey); - } - - static OptionParseResult error(String message) { - return new OptionParseResult(EnumSet.noneOf(OptionType.class), message, null); - } - - static OptionParseResult correct(OptionKey optionKey) { - return new OptionParseResult(EnumSet.noneOf(OptionType.class), null, optionKey); - } - - static OptionParseResult printFlags(EnumSet selectedOptionTypes) { - return new OptionParseResult(selectedOptionTypes, null, null); - } - - static OptionParseResult printFlagsWithExtraHelp(Set optionNameFilter) { - Set optionNames = optionNameFilter; - if (optionNames.contains(EXTRA_HELP_OPTIONS_WILDCARD)) { - optionNames = new HashSet<>(); - optionNames.add(EXTRA_HELP_OPTIONS_WILDCARD); - } - return new OptionParseResult(EnumSet.noneOf(OptionType.class), null, optionNames, null); - } - - boolean printFlags() { - return !printFlags.isEmpty(); - } - - boolean printFlagsWithExtraHelp() { - return !optionNameFilter.isEmpty(); - } - - public boolean isValid() { - boolean result = optionKey != null; - assert result == (printFlags.isEmpty() && optionNameFilter.isEmpty() && error == null); - return result; - } - - public String getError() { - return error; - } - - public OptionKey getOptionKey() { - return optionKey; - } - - private boolean matchesFlags(OptionDescriptor d, boolean svmOption) { - if (!printFlags.isEmpty()) { - boolean showAll = printFlags.equals(EnumSet.allOf(OptionType.class)); - return showAll || svmOption && printFlags.contains(d.getOptionType()); - } - if (!optionNameFilter.isEmpty()) { - if (optionNameFilter.contains(EXTRA_HELP_OPTIONS_WILDCARD) && !d.getExtraHelp().isEmpty()) { - return true; - } - return optionNameFilter.contains(d.getName()); - } - return false; - } - - boolean matchesFlagsRuntime(OptionDescriptor d) { - return matchesFlags(d, d.getOptionKey() instanceof RuntimeOptionKey); - } - - boolean matchesFlagsHosted(OptionDescriptor d) { - OptionKey key = d.getOptionKey(); - return matchesFlags(d, key instanceof RuntimeOptionKey || key instanceof HostedOptionKey); - } - } - - /** - * Constants denoting supported boolean option formats. - */ - public enum BooleanOptionFormat { - NAME_VALUE("="), - PLUS_MINUS("+/-"); - - BooleanOptionFormat(String help) { - this.help = help; - } - - private final String help; - - @Override - public String toString() { - return help; - } - } + public static final String HOSTED_OPTION_PREFIX = CommonOptionParser.HOSTED_OPTION_PREFIX; + public static final String RUNTIME_OPTION_PREFIX = CommonOptionParser.RUNTIME_OPTION_PREFIX; static OptionParseResult parseOption(EconomicMap options, Predicate> isHosted, String option, EconomicMap, Object> valuesMap, String optionPrefix, BooleanOptionFormat booleanOptionFormat) { - if (option.length() == 0) { - return OptionParseResult.error("Option name must be specified"); - } - - String optionName; - Object value = null; - String valueString = null; - - char first = option.charAt(0); - int eqIndex = option.indexOf('='); - if (first == '+' || first == '-') { - if (eqIndex != -1) { - return OptionParseResult.error("Cannot mix +/- with = format: '" + optionPrefix + option + "'"); - } - optionName = option.substring(1); - if (booleanOptionFormat == BooleanOptionFormat.NAME_VALUE) { - return OptionParseResult.error("Option " + LocatableOption.from(optionName) + " must use = format, not +/- prefix"); - } - value = (first == '+'); - } else { - if (eqIndex == -1) { - optionName = option; - valueString = null; - } else { - optionName = option.substring(0, eqIndex); - valueString = option.substring(eqIndex + 1); - } - } - - LocatableOption current = LocatableOption.from(optionName); - OptionDescriptor desc = options.get(current.name); - if (desc == null && value != null) { - if (eqIndex != -1) { - optionName = option.substring(1, eqIndex); - current = LocatableOption.from(optionName); - desc = options.get(current.name); - } - } - - optionName = current.name; - - if (desc == null) { - List matches = new ArrayList<>(); - OptionsParser.collectFuzzyMatches(options.getValues(), optionName, matches); - StringBuilder msg = new StringBuilder("Could not find option ").append(current); - if (!matches.isEmpty()) { - msg.append(". Did you mean one of these:"); - for (OptionDescriptor match : matches) { - msg.append(' ').append(match.getName()); - } - } - msg.append(". Use ").append(optionPrefix).append(SubstrateOptions.PrintFlags.getName()).append("= to list all available options."); - return OptionParseResult.error(msg.toString()); - } - - OptionKey optionKey = desc.getOptionKey(); - boolean hostedOption = isHosted.test(optionKey); - Class optionValueType = getMultiOptionValueElementType(optionKey); - Class optionType = hostedOption && optionValueType != null ? optionValueType : desc.getOptionValueType(); - - if (value == null) { - if (optionType == Boolean.class && booleanOptionFormat == BooleanOptionFormat.PLUS_MINUS) { - return OptionParseResult.error("Boolean option " + current + " must have +/- prefix"); - } - if (valueString == null) { - return OptionParseResult.error("Missing value for option " + current); - } - try { - value = parseValue(optionType, current, valueString); - if (value instanceof OptionParseResult) { - return (OptionParseResult) value; - } - } catch (NumberFormatException ex) { - return OptionParseResult.error("Invalid value for option " + current + ": '" + valueString + "' is not a valid number"); - } - } else { - if (optionType != Boolean.class) { - return OptionParseResult.error("Non-boolean option " + current + " can not use +/- prefix. Use '" + current.name + "=' format"); - } - } - - optionKey.update(valuesMap, hostedOption ? LocatableOption.value(value, current.origin) : value); - - if (SubstrateOptions.PrintFlags.getName().equals(optionName)) { - String optionValue = (String) value; - EnumSet selectedOptionTypes; - if (optionValue.isEmpty()) { - selectedOptionTypes = EnumSet.allOf(OptionType.class); - } else { - selectedOptionTypes = EnumSet.noneOf(OptionType.class); - String enumString = null; - try { - String[] enumStrings = SubstrateUtil.split(optionValue, ","); - - for (String string : enumStrings) { - enumString = string; - selectedOptionTypes.add(OptionType.valueOf(enumString)); - } - } catch (IllegalArgumentException e) { - StringBuilder sb = new StringBuilder(); - boolean firstValue = true; - for (OptionType ot : OptionType.values()) { - if (firstValue) { - firstValue = false; - } else { - sb.append(", "); - } - sb.append(ot.name()); - } - String possibleValues = sb.toString(); - return OptionParseResult.error("Invalid value for option " + current + ". '" + enumString + "' is not one of: " + possibleValues); - } - } - return OptionParseResult.printFlags(selectedOptionTypes); - } - if (SubstrateOptions.PrintFlagsWithExtraHelp.getName().equals(optionName)) { - String optionValue = (String) value; - String[] optionNames = SubstrateUtil.split(optionValue, ","); - HashSet selectedOptionNames = new HashSet<>(Arrays.asList(optionNames)); - return OptionParseResult.printFlagsWithExtraHelp(selectedOptionNames); - } - - return OptionParseResult.correct(optionKey); - } - - private static Class getMultiOptionValueElementType(OptionKey optionKey) { - Object defaultValue = optionKey.getDefaultValue(); - if (defaultValue instanceof MultiOptionValue) { - return ((MultiOptionValue) defaultValue).getValueType(); - } - return null; - } - - @SuppressWarnings("unchecked") - static Object parseValue(Class optionType, LocatableOption option, String valueString) throws NumberFormatException { - Object value; - if (optionType == Integer.class) { - long longValue = parseLong(valueString); - if ((int) longValue != longValue) { - return OptionParseResult.error("Wrong value for option " + option + ": '" + valueString + "' is not a valid number"); - } - value = (int) longValue; - } else if (optionType == Long.class) { - value = parseLong(valueString); - } else if (optionType == String.class) { - value = valueString; - } else if (optionType == Double.class) { - value = parseDouble(valueString); - } else if (optionType == Boolean.class) { - if (valueString.equals("true")) { - value = true; - } else if (valueString.equals("false")) { - value = false; - } else { - return OptionParseResult.error("Boolean option " + option + " must have value 'true' or 'false'"); - } - } else if (optionType.isEnum()) { - value = Enum.valueOf(optionType.asSubclass(Enum.class), valueString); - } else { - throw VMError.shouldNotReachHere(option + " uses unsupported option value class: " + ClassUtil.getUnqualifiedName(optionType)); + try { + return CommonOptionParser.parseOption(options, isHosted, option, valuesMap, optionPrefix, booleanOptionFormat); + } catch (UnsupportedOptionClassException e) { + VMError.shouldNotReachHere(e.getMessage()); + return null; } - return value; } /** @@ -359,7 +90,10 @@ public static boolean parseHostedOption(String optionPrefix, EconomicMap optionParseResult.matchesFlags(d, () -> { + OptionKey key = d.getOptionKey(); + return key instanceof RuntimeOptionKey || key instanceof HostedOptionKey; + }), options, optionPrefix, out, optionParseResult.printFlagsWithExtraHelp()); throw new InterruptImageBuilding(""); } if (!optionParseResult.isValid()) { @@ -383,46 +117,12 @@ public static boolean parseHostedOption(String optionPrefix, EconomicMap optionDescriptors, Consumer optionDescriptorConsumer) { + CommonOptionParser.collectOptions(optionDescriptors, optionDescriptorConsumer); } public static void printOption(Consumer println, String option, String description, int indentation, int optionWidth, int wrapWidth) { - String indent = spaces(indentation); - String desc = description != null ? description : ""; - desc = wrapWidth > 0 ? wrap(desc, wrapWidth) : desc; - String nl = System.lineSeparator(); - String[] descLines = SubstrateUtil.split(desc, nl); - if (option.length() >= optionWidth && description != null) { - println.accept(indent + option + nl + indent + spaces(optionWidth) + descLines[0]); - } else { - println.accept(indent + option + spaces(optionWidth - option.length()) + descLines[0]); - } - for (int i = 1; i < descLines.length; i++) { - println.accept(indent + spaces(optionWidth) + descLines[i]); - } + CommonOptionParser.printOption(println, option, description, indentation, optionWidth, wrapWidth); } /** @@ -441,151 +141,15 @@ public static void printOption(Consumer println, String option, String d * between space and execution efficiency is acceptable. */ static void printFlags(Predicate filter, EconomicMap options, String prefix, PrintStream out, boolean verbose) { - List sortedDescriptors = new ArrayList<>(); - for (OptionDescriptor option : options.getValues()) { - if (filter.test(option)) { - sortedDescriptors.add(option); - } - } - sortedDescriptors.sort(Comparator.comparing(OptionDescriptor::getName)); - - for (OptionDescriptor descriptor : sortedDescriptors) { - String helpMsg = verbose && !descriptor.getExtraHelp().isEmpty() ? "" : descriptor.getHelp(); - int helpLen = helpMsg.length(); - if (helpLen > 0 && helpMsg.charAt(helpLen - 1) != '.') { - helpMsg += '.'; - } - boolean stringifiedArrayValue = false; - Object defaultValue = descriptor.getOptionKey().getDefaultValue(); - if (defaultValue != null && defaultValue.getClass().isArray()) { - Object[] defaultValues = (Object[]) defaultValue; - if (defaultValues.length == 1) { - defaultValue = defaultValues[0]; - } else { - List stringList = new ArrayList<>(); - String optionPrefix = prefix + descriptor.getName() + "="; - for (Object rawValue : defaultValues) { - String value; - if (rawValue instanceof String) { - value = '"' + String.valueOf(rawValue) + '"'; - } else { - value = String.valueOf(rawValue); - } - stringList.add(optionPrefix + value); - } - if (helpLen != 0) { - helpMsg += ' '; - } - helpMsg += "Default: "; - if (stringList.isEmpty()) { - helpMsg += "None"; - } else { - helpMsg += String.join(" ", stringList); - } - stringifiedArrayValue = true; - } - } - String verboseHelp = ""; - if (verbose) { - verboseHelp = System.lineSeparator() + descriptor.getHelp() + System.lineSeparator() + String.join(System.lineSeparator(), descriptor.getExtraHelp()); - } else if (!descriptor.getExtraHelp().isEmpty()) { - verboseHelp = " [Extra help available]"; - } - int wrapWidth = verbose ? 0 : PRINT_OPTION_WRAP_WIDTH; - if (descriptor.getOptionValueType() == Boolean.class) { - Boolean val = (Boolean) defaultValue; - if (helpLen != 0) { - helpMsg += ' '; - } - if (val != null) { - if (val) { - helpMsg += "Default: + (enabled)."; - } else { - helpMsg += "Default: - (disabled)."; - } - } - printOption(out, prefix + "\u00b1" + descriptor.getName(), helpMsg + verboseHelp, wrapWidth); - } else { - if (defaultValue == null) { - if (helpLen != 0) { - helpMsg += ' '; - } - helpMsg += "Default: None"; - } - helpMsg += verboseHelp; - if (stringifiedArrayValue || defaultValue == null) { - printOption(out, prefix + descriptor.getName() + "=...", helpMsg, wrapWidth); - } else { - if (defaultValue instanceof String) { - defaultValue = '"' + String.valueOf(defaultValue) + '"'; - } - printOption(out, prefix + descriptor.getName() + "=" + defaultValue, helpMsg, wrapWidth); - } - } - } + CommonOptionParser.printFlags(filter, options, prefix, out, verbose); } public static long parseLong(String v) { - String valueString = v.trim().toLowerCase(); - long scale = 1; - if (valueString.endsWith("k")) { - scale = 1024L; - } else if (valueString.endsWith("m")) { - scale = 1024L * 1024L; - } else if (valueString.endsWith("g")) { - scale = 1024L * 1024L * 1024L; - } else if (valueString.endsWith("t")) { - scale = 1024L * 1024L * 1024L * 1024L; - } - - if (scale != 1) { - /* Remove trailing scale character. */ - valueString = valueString.substring(0, valueString.length() - 1); - } - - return Long.parseLong(valueString) * scale; + return CommonOptionParser.parseLong(v); } - /** - * Parses the provide string to a double number, avoiding the JDK dependencies (which pull in a - * lot of classes, including the regular expression library). Only simple numbers are supported, - * without fancy exponent styles. - */ public static double parseDouble(String v) { - String valueString = v.trim(); - - int dotPos = valueString.indexOf('.'); - if (dotPos == -1) { - return parseLong(valueString); - } - - String beforeDot = valueString.substring(0, dotPos); - String afterDot = valueString.substring(dotPos + 1); - - double sign = 1; - if (beforeDot.startsWith("-")) { - sign = -1; - beforeDot = beforeDot.substring(1); - } else if (beforeDot.startsWith("+")) { - beforeDot = beforeDot.substring(1); - } - - if (beforeDot.startsWith("-") || beforeDot.startsWith("+") || afterDot.startsWith("-") || afterDot.startsWith("+") || - (beforeDot.length() == 0 && afterDot.length() == 0)) { - throw new NumberFormatException(v); - } - - double integral = 0; - if (beforeDot.length() > 0) { - integral = Long.parseLong(beforeDot); - } - - double fraction = 0; - if (afterDot.length() > 0) { - fraction = Long.parseLong(afterDot) * Math.pow(10, -afterDot.length()); - } - - return sign * (integral + fraction); + return CommonOptionParser.parseDouble(v); } /** diff --git a/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/NativeImage.java b/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/NativeImage.java index 1373baa0d0c9..c7c7e5b08aa8 100644 --- a/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/NativeImage.java +++ b/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/NativeImage.java @@ -64,6 +64,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import com.oracle.svm.common.option.CommonOptions; import org.graalvm.compiler.options.OptionKey; import org.graalvm.compiler.serviceprovider.JavaVersionUtil; import org.graalvm.nativeimage.Platform; @@ -188,8 +189,8 @@ void addFallbackBuildArgs(@SuppressWarnings("unused") List buildArgs) { public static final String oH = "-H:"; static final String oR = "-R:"; - final String enablePrintFlags = SubstrateOptions.PrintFlags.getName(); - final String enablePrintFlagsWithExtraHelp = SubstrateOptions.PrintFlagsWithExtraHelp.getName(); + final String enablePrintFlags = CommonOptions.PrintFlags.getName(); + final String enablePrintFlagsWithExtraHelp = CommonOptions.PrintFlagsWithExtraHelp.getName(); private static String oH(OptionKey option) { return oH + option.getName() + "="; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/config/ConfigurationParserUtils.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/config/ConfigurationParserUtils.java index f5e2a1b5d56f..193c309a935a 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/config/ConfigurationParserUtils.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/config/ConfigurationParserUtils.java @@ -24,7 +24,7 @@ */ package com.oracle.svm.hosted.config; -import static com.oracle.svm.core.SubstrateOptions.PrintFlags; +import static com.oracle.svm.common.option.CommonOptions.PrintFlags; import java.io.IOException; import java.io.InputStreamReader; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/option/HostedOptionParser.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/option/HostedOptionParser.java index 657da655ed43..e9a795dc44a3 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/option/HostedOptionParser.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/option/HostedOptionParser.java @@ -24,7 +24,7 @@ */ package com.oracle.svm.hosted.option; -import static com.oracle.svm.core.option.SubstrateOptionsParser.BooleanOptionFormat.PLUS_MINUS; +import static com.oracle.svm.common.option.CommonOptionParser.BooleanOptionFormat.PLUS_MINUS; import static com.oracle.svm.core.util.VMError.shouldNotReachHere; import java.util.ArrayList; @@ -59,30 +59,28 @@ public HostedOptionParser(ClassLoader imageClassLoader) { } public static void collectOptions(ServiceLoader optionDescriptors, EconomicMap allHostedOptions, - EconomicMap allRuntimeOptions) { - for (OptionDescriptors optionDescriptor : optionDescriptors) { - for (OptionDescriptor descriptor : optionDescriptor) { - String name = descriptor.getName(); - - if (descriptor.getDeclaringClass().getAnnotation(Platforms.class) != null) { - throw UserError.abort("Options must not be declared in a class that has a @%s annotation: option %s declared in %s", - Platforms.class.getSimpleName(), name, descriptor.getDeclaringClass().getTypeName()); - } + EconomicMap allRuntimeOptions) { + SubstrateOptionsParser.collectOptions(optionDescriptors, descriptor -> { + String name = descriptor.getName(); + + if (descriptor.getDeclaringClass().getAnnotation(Platforms.class) != null) { + throw UserError.abort("Options must not be declared in a class that has a @%s annotation: option %s declared in %s", + Platforms.class.getSimpleName(), name, descriptor.getDeclaringClass().getTypeName()); + } - if (!(descriptor.getOptionKey() instanceof RuntimeOptionKey)) { - OptionDescriptor existing = allHostedOptions.put(name, descriptor); - if (existing != null) { - throw shouldNotReachHere("Option name \"" + name + "\" has multiple definitions: " + existing.getLocation() + " and " + descriptor.getLocation()); - } + if (!(descriptor.getOptionKey() instanceof RuntimeOptionKey)) { + OptionDescriptor existing = allHostedOptions.put(name, descriptor); + if (existing != null) { + throw shouldNotReachHere("Option name \"" + name + "\" has multiple definitions: " + existing.getLocation() + " and " + descriptor.getLocation()); } - if (!(descriptor.getOptionKey() instanceof HostedOptionKey)) { - OptionDescriptor existing = allRuntimeOptions.put(name, descriptor); - if (existing != null) { - throw shouldNotReachHere("Option name \"" + name + "\" has multiple definitions: " + existing.getLocation() + " and " + descriptor.getLocation()); - } + } + if (!(descriptor.getOptionKey() instanceof HostedOptionKey)) { + OptionDescriptor existing = allRuntimeOptions.put(name, descriptor); + if (existing != null) { + throw shouldNotReachHere("Option name \"" + name + "\" has multiple definitions: " + existing.getLocation() + " and " + descriptor.getLocation()); } } - } + }); } public List parse(List args) { diff --git a/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/StringUtil.java b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/StringUtil.java new file mode 100644 index 000000000000..602484d32ab5 --- /dev/null +++ b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/StringUtil.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2021, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2021, Alibaba Group Holding Limited. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.oracle.svm.util; + +import java.util.ArrayList; + +public class StringUtil { + + /** + * Similar to {@link String#split(String)} but with a fixed separator string instead of a + * regular expression. This avoids making regular expression code reachable. + */ + public static String[] split(String value, String separator) { + return split(value, separator, 0); + } + + /** + * Similar to {@link String#split(String, int)} but with a fixed separator string instead of a + * regular expression. This avoids making regular expression code reachable. + */ + public static String[] split(String value, String separator, int limit) { + int offset = 0; + int next; + ArrayList list = null; + while ((next = value.indexOf(separator, offset)) != -1) { + if (list == null) { + list = new ArrayList<>(); + } + boolean limited = limit > 0; + if (!limited || list.size() < limit - 1) { + list.add(value.substring(offset, next)); + offset = next + separator.length(); + } else { + break; + } + } + + if (offset == 0) { + /* No match found. */ + return new String[]{value}; + } + + /* Add remaining segment. */ + list.add(value.substring(offset)); + + return list.toArray(new String[list.size()]); + } +}