1 /* 2 * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 25 package org.graalvm.compiler.options; 26 27 import java.util.ArrayList; 28 import java.util.Collection; 29 import java.util.Formatter; 30 import java.util.List; 31 import java.util.ServiceLoader; 32 33 import jdk.internal.vm.compiler.collections.EconomicMap; 34 import jdk.internal.vm.compiler.collections.MapCursor; 35 36 /** 37 * This class contains methods for parsing Graal options and matching them against a set of 38 * {@link OptionDescriptors}. The {@link OptionDescriptors} are loaded via a {@link ServiceLoader}. 39 */ 40 public class OptionsParser { 41 42 /** 43 * Gets an iterable composed of the {@link ServiceLoader}s to be used when looking for 44 * {@link OptionDescriptors} providers. 45 */ 46 public static Iterable<OptionDescriptors> getOptionsLoader() { 47 boolean java8OrEarlier = System.getProperty("java.specification.version").compareTo("1.9") < 0; 48 ClassLoader loader; 49 if (java8OrEarlier) { 50 // On JDK 8, Graal and its extensions are loaded by same class loader. 51 loader = OptionDescriptors.class.getClassLoader(); 52 } else { 53 /* 54 * The Graal module (i.e., jdk.internal.vm.compiler) is loaded by the platform class 55 * loader as of JDK 9. Modules that depend on and extend Graal are loaded by the app 56 * class loader. As such, we need to start the provider search at the app class loader 57 * instead of the platform class loader. 58 */ 59 loader = ClassLoader.getSystemClassLoader(); 60 } 61 return ServiceLoader.load(OptionDescriptors.class, loader); 62 } 63 64 /** 65 * Parses a map representing assignments of values to options. 66 * 67 * @param optionSettings option settings (i.e., assignments of values to options) 68 * @param values the object in which to store the parsed values 69 * @param loader source of the available {@link OptionDescriptors} 70 * @throws IllegalArgumentException if there's a problem parsing {@code option} 71 */ 72 public static void parseOptions(EconomicMap<String, String> optionSettings, EconomicMap<OptionKey<?>, Object> values, Iterable<OptionDescriptors> loader) { 73 if (optionSettings != null && !optionSettings.isEmpty()) { 74 MapCursor<String, String> cursor = optionSettings.getEntries(); 75 while (cursor.advance()) { 76 parseOption(cursor.getKey(), cursor.getValue(), values, loader); 77 } 78 } 79 } 80 81 /** 82 * Parses a given option setting string and adds the parsed key and value {@code dst}. 83 * 84 * @param optionSetting a string matching the pattern {@code <name>=<value>} 85 */ 86 public static void parseOptionSettingTo(String optionSetting, EconomicMap<String, String> dst) { 87 int eqIndex = optionSetting.indexOf('='); 88 if (eqIndex == -1) { 89 throw new InternalError("Option setting has does not match the pattern <name>=<value>: " + optionSetting); 90 } 91 dst.put(optionSetting.substring(0, eqIndex), optionSetting.substring(eqIndex + 1)); 92 } 93 94 /** 95 * Looks up an {@link OptionDescriptor} based on a given name. 96 * 97 * @param loader source of the available {@link OptionDescriptors} 98 * @param name the name of the option to look up 99 * @return the {@link OptionDescriptor} whose name equals {@code name} or null if not such 100 * descriptor is available 101 */ 102 private static OptionDescriptor lookup(Iterable<OptionDescriptors> loader, String name) { 103 for (OptionDescriptors optionDescriptors : loader) { 104 OptionDescriptor desc = optionDescriptors.get(name); 105 if (desc != null) { 106 return desc; 107 } 108 } 109 return null; 110 } 111 112 /** 113 * Parses a given option name and value. 114 * 115 * @param name the option name 116 * @param uncheckedValue the unchecked value for the option 117 * @param values the object in which to store the parsed option and value 118 * @param loader source of the available {@link OptionDescriptors} 119 * @throws IllegalArgumentException if there's a problem parsing {@code option} 120 */ 121 public static void parseOption(String name, Object uncheckedValue, EconomicMap<OptionKey<?>, Object> values, Iterable<OptionDescriptors> loader) { 122 123 OptionDescriptor desc = lookup(loader, name); 124 if (desc == null) { 125 List<OptionDescriptor> matches = fuzzyMatch(loader, name); 126 Formatter msg = new Formatter(); 127 msg.format("Could not find option %s", name); 128 if (!matches.isEmpty()) { 129 msg.format("%nDid you mean one of the following?"); 130 for (OptionDescriptor match : matches) { 131 msg.format("%n %s=<value>", match.getName()); 132 } 133 } 134 throw new IllegalArgumentException(msg.toString()); 135 } 136 137 Class<?> optionType = desc.getOptionValueType(); 138 Object value; 139 if (!(uncheckedValue instanceof String)) { 140 if (optionType != uncheckedValue.getClass()) { 141 String type = optionType.getSimpleName(); 142 throw new IllegalArgumentException(type + " option '" + name + "' must have " + type + " value, not " + uncheckedValue.getClass() + " [toString: " + uncheckedValue + "]"); 143 } 144 value = uncheckedValue; 145 } else { 146 String valueString = (String) uncheckedValue; 147 if (optionType == Boolean.class) { 148 if ("true".equals(valueString)) { 149 value = Boolean.TRUE; 150 } else if ("false".equals(valueString)) { 151 value = Boolean.FALSE; 152 } else { 153 throw new IllegalArgumentException("Boolean option '" + name + "' must have value \"true\" or \"false\", not \"" + uncheckedValue + "\""); 154 } 155 } else if (optionType == String.class) { 156 value = valueString; 157 } else if (Enum.class.isAssignableFrom(optionType)) { 158 value = ((EnumOptionKey<?>) desc.getOptionKey()).valueOf(valueString); 159 } else { 160 if (valueString.isEmpty()) { 161 throw new IllegalArgumentException("Non empty value required for option '" + name + "'"); 162 } 163 try { 164 if (optionType == Float.class) { 165 value = Float.parseFloat(valueString); 166 } else if (optionType == Double.class) { 167 value = Double.parseDouble(valueString); 168 } else if (optionType == Integer.class) { 169 value = Integer.valueOf((int) parseLong(valueString)); 170 } else if (optionType == Long.class) { 171 value = Long.valueOf(parseLong(valueString)); 172 } else { 173 throw new IllegalArgumentException("Wrong value for option '" + name + "'"); 174 } 175 } catch (NumberFormatException nfe) { 176 throw new IllegalArgumentException("Value for option '" + name + "' has invalid number format: " + valueString); 177 } 178 } 179 } 180 181 desc.optionKey.update(values, value); 182 } 183 184 private static long parseLong(String v) { 185 String valueString = v.toLowerCase(); 186 long scale = 1; 187 if (valueString.endsWith("k")) { 188 scale = 1024L; 189 } else if (valueString.endsWith("m")) { 190 scale = 1024L * 1024L; 191 } else if (valueString.endsWith("g")) { 192 scale = 1024L * 1024L * 1024L; 193 } else if (valueString.endsWith("t")) { 194 scale = 1024L * 1024L * 1024L * 1024L; 195 } 196 197 if (scale != 1) { 198 /* Remove trailing scale character. */ 199 valueString = valueString.substring(0, valueString.length() - 1); 200 } 201 202 return Long.parseLong(valueString) * scale; 203 } 204 205 /** 206 * Compute string similarity based on Dice's coefficient. 207 * 208 * Ported from str_similar() in globals.cpp. 209 */ 210 static float stringSimiliarity(String str1, String str2) { 211 int hit = 0; 212 for (int i = 0; i < str1.length() - 1; ++i) { 213 for (int j = 0; j < str2.length() - 1; ++j) { 214 if ((str1.charAt(i) == str2.charAt(j)) && (str1.charAt(i + 1) == str2.charAt(j + 1))) { 215 ++hit; 216 break; 217 } 218 } 219 } 220 return 2.0f * hit / (str1.length() + str2.length()); 221 } 222 223 private static final float FUZZY_MATCH_THRESHOLD = 0.7F; 224 225 /** 226 * Returns the set of options that fuzzy match a given option name. 227 */ 228 private static List<OptionDescriptor> fuzzyMatch(Iterable<OptionDescriptors> loader, String optionName) { 229 List<OptionDescriptor> matches = new ArrayList<>(); 230 for (OptionDescriptors options : loader) { 231 collectFuzzyMatches(options, optionName, matches); 232 } 233 return matches; 234 } 235 236 /** 237 * Collects the set of options that fuzzy match a given option name. String similarity for fuzzy 238 * matching is based on Dice's coefficient. 239 * 240 * @param toSearch the set of option descriptors to search 241 * @param name the option name to search for 242 * @param matches the collection to which fuzzy matches of {@code name} will be added 243 * @return whether any fuzzy matches were found 244 */ 245 public static boolean collectFuzzyMatches(Iterable<OptionDescriptor> toSearch, String name, Collection<OptionDescriptor> matches) { 246 boolean found = false; 247 for (OptionDescriptor option : toSearch) { 248 float score = stringSimiliarity(option.getName(), name); 249 if (score >= FUZZY_MATCH_THRESHOLD) { 250 found = true; 251 matches.add(option); 252 } 253 } 254 return found; 255 } 256 }