1 //===--- FuzzyMatch.h - Approximate identifier matching ---------*- C++-*-===// 2 // 3 // The LLVM Compiler Infrastructure 4 // 5 // This file is distributed under the University of Illinois Open Source 6 // License. See LICENSE.TXT for details. 7 // 8 //===----------------------------------------------------------------------===// 9 // 10 // To check for a match between a Pattern ('u_p') and a Word ('unique_ptr'), 11 // we consider the possible partial match states: 12 // 13 // u n i q u e _ p t r 14 // +--------------------- 15 // |A . . . . . . . . . . 16 // u| 17 // |. . . . . . . . . . . 18 // _| 19 // |. . . . . . . O . . . 20 // p| 21 // |. . . . . . . . . . B 22 // 23 // Each dot represents some prefix of the pattern being matched against some 24 // prefix of the word. 25 // - A is the initial state: '' matched against '' 26 // - O is an intermediate state: 'u_' matched against 'unique_' 27 // - B is the target state: 'u_p' matched against 'unique_ptr' 28 // 29 // We aim to find the best path from A->B. 30 // - Moving right (consuming a word character) 31 // Always legal: not all word characters must match. 32 // - Moving diagonally (consuming both a word and pattern character) 33 // Legal if the characters match. 34 // - Moving down (consuming a pattern character) is never legal. 35 // Never legal: all pattern characters must match something. 36 // 37 // The scoring is based on heuristics: 38 // - when matching a character, apply a bonus or penalty depending on the 39 // match quality (does case match, do word segments align, etc) 40 // - when skipping a character, apply a penalty if it hurts the match 41 // (it starts a word segment, or splits the matched region, etc) 42 // 43 // These heuristics require the ability to "look backward" one character, to 44 // see whether it was matched or not. Therefore the dynamic-programming matrix 45 // has an extra dimension (last character matched). 46 // Each entry also has an additional flag indicating whether the last-but-one 47 // character matched, which is needed to trace back through the scoring table 48 // and reconstruct the match. 49 // 50 // We treat strings as byte-sequences, so only ASCII has first-class support. 51 // 52 // This algorithm was inspired by VS code's client-side filtering, and aims 53 // to be mostly-compatible. 54 // 55 //===----------------------------------------------------------------------===// 56 57 #include "FuzzyMatch.h" 58 #include "llvm/ADT/Optional.h" 59 #include "llvm/Support/Format.h" 60 61 namespace clang { 62 namespace clangd { 63 using namespace llvm; 64 65 constexpr int FuzzyMatcher::MaxPat; 66 constexpr int FuzzyMatcher::MaxWord; 67 68 static char lower(char C) { return C >= 'A' && C <= 'Z' ? C + ('a' - 'A') : C; } 69 // A "negative infinity" score that won't overflow. 70 // We use this to mark unreachable states and forbidden solutions. 71 // Score field is 15 bits wide, min value is -2^14, we use half of that. 72 static constexpr int AwfulScore = -(1 << 13); 73 static bool isAwful(int S) { return S < AwfulScore / 2; } 74 static constexpr int PerfectBonus = 3; // Perfect per-pattern-char score. 75 76 FuzzyMatcher::FuzzyMatcher(StringRef Pattern) 77 : PatN(std::min<int>(MaxPat, Pattern.size())), CaseSensitive(false), 78 ScoreScale(PatN ? float{1} / (PerfectBonus * PatN) : 0), WordN(0) { 79 std::copy(Pattern.begin(), Pattern.begin() + PatN, Pat); 80 for (int I = 0; I < PatN; ++I) { 81 LowPat[I] = lower(Pat[I]); 82 CaseSensitive |= LowPat[I] != Pat[I]; 83 } 84 Scores[0][0][Miss] = {0, Miss}; 85 Scores[0][0][Match] = {AwfulScore, Miss}; 86 for (int P = 0; P <= PatN; ++P) 87 for (int W = 0; W < P; ++W) 88 for (Action A : {Miss, Match}) 89 Scores[P][W][A] = {AwfulScore, Miss}; 90 if (PatN > 0) 91 calculateRoles(Pat, PatRole, PatN); 92 } 93 94 Optional<float> FuzzyMatcher::match(StringRef Word) { 95 if (!(WordContainsPattern = init(Word))) 96 return None; 97 if (!PatN) 98 return 1; 99 buildGraph(); 100 auto Best = std::max(Scores[PatN][WordN][Miss].Score, 101 Scores[PatN][WordN][Match].Score); 102 if (isAwful(Best)) 103 return None; 104 return ScoreScale * std::min(PerfectBonus * PatN, std::max<int>(0, Best)); 105 } 106 107 // Segmentation of words and patterns. 108 // A name like "fooBar_baz" consists of several parts foo, bar, baz. 109 // Aligning segmentation of word and pattern improves the fuzzy-match. 110 // For example: [lol] matches "LaughingOutLoud" better than "LionPopulation" 111 // 112 // First we classify each character into types (uppercase, lowercase, etc). 113 // Then we look at the sequence: e.g. [upper, lower] is the start of a segment. 114 115 // We only distinguish the types of characters that affect segmentation. 116 // It's not obvious how to segment digits, we treat them as lowercase letters. 117 // As we don't decode UTF-8, we treat bytes over 127 as lowercase too. 118 // This means we require exact (case-sensitive) match. 119 enum FuzzyMatcher::CharType : unsigned char { 120 Empty = 0, // Before-the-start and after-the-end (and control chars). 121 Lower = 1, // Lowercase letters, digits, and non-ASCII bytes. 122 Upper = 2, // Uppercase letters. 123 Punctuation = 3, // ASCII punctuation (including Space) 124 }; 125 126 // We get CharTypes from a lookup table. Each is 2 bits, 4 fit in each byte. 127 // The top 6 bits of the char select the byte, the bottom 2 select the offset. 128 // e.g. 'q' = 010100 01 = byte 28 (55), bits 3-2 (01) -> Lower. 129 constexpr static uint8_t CharTypes[] = { 130 0x00, 0x00, 0x00, 0x00, // Control characters 131 0x00, 0x00, 0x00, 0x00, // Control characters 132 0xff, 0xff, 0xff, 0xff, // Punctuation 133 0x55, 0x55, 0xf5, 0xff, // Numbers->Lower, more Punctuation. 134 0xab, 0xaa, 0xaa, 0xaa, // @ and A-O 135 0xaa, 0xaa, 0xea, 0xff, // P-Z, more Punctuation. 136 0x57, 0x55, 0x55, 0x55, // ` and a-o 137 0x55, 0x55, 0xd5, 0x3f, // p-z, Punctuation, DEL. 138 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, // Bytes over 127 -> Lower. 139 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, // (probably UTF-8). 140 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 141 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 142 }; 143 144 // Each character's Role is the Head or Tail of a segment, or a Separator. 145 // e.g. XMLHttpRequest_Async 146 // +--+---+------ +---- 147 // ^Head ^Tail ^Separator 148 enum FuzzyMatcher::CharRole : unsigned char { 149 Unknown = 0, // Stray control characters or impossible states. 150 Tail = 1, // Part of a word segment, but not the first character. 151 Head = 2, // The first character of a word segment. 152 Separator = 3, // Punctuation characters that separate word segments. 153 }; 154 155 // The Role can be determined from the Type of a character and its neighbors: 156 // 157 // Example | Chars | Type | Role 158 // ---------+--------------+----- 159 // F(o)oBar | Foo | Ull | Tail 160 // Foo(B)ar | oBa | lUl | Head 161 // (f)oo | ^fo | Ell | Head 162 // H(T)TP | HTT | UUU | Tail 163 // 164 // Our lookup table maps a 6 bit key (Prev, Curr, Next) to a 2-bit Role. 165 // A byte packs 4 Roles. (Prev, Curr) selects a byte, Next selects the offset. 166 // e.g. Lower, Upper, Lower -> 01 10 01 -> byte 6 (aa), bits 3-2 (10) -> Head. 167 constexpr static uint8_t CharRoles[] = { 168 // clang-format off 169 // Curr= Empty Lower Upper Separ 170 /* Prev=Empty */ 0x00, 0xaa, 0xaa, 0xff, // At start, Lower|Upper->Head 171 /* Prev=Lower */ 0x00, 0x55, 0xaa, 0xff, // In word, Upper->Head;Lower->Tail 172 /* Prev=Upper */ 0x00, 0x55, 0x59, 0xff, // Ditto, but U(U)U->Tail 173 /* Prev=Separ */ 0x00, 0xaa, 0xaa, 0xff, // After separator, like at start 174 // clang-format on 175 }; 176 177 template <typename T> static T packedLookup(const uint8_t *Data, int I) { 178 return static_cast<T>((Data[I >> 2] >> ((I & 3) * 2)) & 3); 179 } 180 void FuzzyMatcher::calculateRoles(const char *Text, CharRole *Out, int N) { 181 assert(N > 0); 182 // Types holds a sliding window of (Prev, Curr, Next) types. 183 // Initial value is (Empty, Empty, type of Text[0]). 184 int Types = packedLookup<CharType>(CharTypes, Text[0]); 185 // Rotate slides in the type of the next character. 186 auto Rotate = [&](CharType T) { Types = ((Types << 2) | T) & 0x3f; }; 187 for (int I = 0; I < N - 1; ++I) { 188 // For each character, rotate in the next, and look up the role. 189 Rotate(packedLookup<CharType>(CharTypes, Text[I + 1])); 190 *Out++ = packedLookup<CharRole>(CharRoles, Types); 191 } 192 // For the last character, the "next character" is Empty. 193 Rotate(Empty); 194 *Out++ = packedLookup<CharRole>(CharRoles, Types); 195 } 196 197 // Sets up the data structures matching Word. 198 // Returns false if we can cheaply determine that no match is possible. 199 bool FuzzyMatcher::init(StringRef NewWord) { 200 WordN = std::min<int>(MaxWord, NewWord.size()); 201 if (PatN > WordN) 202 return false; 203 std::copy(NewWord.begin(), NewWord.begin() + WordN, Word); 204 if (PatN == 0) 205 return true; 206 for (int I = 0; I < WordN; ++I) 207 LowWord[I] = lower(Word[I]); 208 209 // Cheap subsequence check. 210 for (int W = 0, P = 0; P != PatN; ++W) { 211 if (W == WordN) 212 return false; 213 if (LowWord[W] == LowPat[P]) 214 ++P; 215 } 216 217 calculateRoles(Word, WordRole, WordN); 218 return true; 219 } 220 221 // The forwards pass finds the mappings of Pattern onto Word. 222 // Score = best score achieved matching Word[..W] against Pat[..P]. 223 // Unlike other tables, indices range from 0 to N *inclusive* 224 // Matched = whether we chose to match Word[W] with Pat[P] or not. 225 // 226 // Points are mostly assigned to matched characters, with 1 being a good score 227 // and 3 being a great one. So we treat the score range as [0, 3 * PatN]. 228 // This range is not strict: we can apply larger bonuses/penalties, or penalize 229 // non-matched characters. 230 void FuzzyMatcher::buildGraph() { 231 for (int W = 0; W < WordN; ++W) { 232 Scores[0][W + 1][Miss] = {Scores[0][W][Miss].Score - skipPenalty(W, Miss), 233 Miss}; 234 Scores[0][W + 1][Match] = {AwfulScore, Miss}; 235 } 236 for (int P = 0; P < PatN; ++P) { 237 for (int W = P; W < WordN; ++W) { 238 auto &Score = Scores[P + 1][W + 1], &PreMiss = Scores[P + 1][W]; 239 240 auto MatchMissScore = PreMiss[Match].Score; 241 auto MissMissScore = PreMiss[Miss].Score; 242 if (P < PatN - 1) { // Skipping trailing characters is always free. 243 MatchMissScore -= skipPenalty(W, Match); 244 MissMissScore -= skipPenalty(W, Miss); 245 } 246 Score[Miss] = (MatchMissScore > MissMissScore) 247 ? ScoreInfo{MatchMissScore, Match} 248 : ScoreInfo{MissMissScore, Miss}; 249 250 if (LowPat[P] != LowWord[W]) { // No match possible. 251 Score[Match] = {AwfulScore, Miss}; 252 } else { 253 auto &PreMatch = Scores[P][W]; 254 auto MatchMatchScore = PreMatch[Match].Score + matchBonus(P, W, Match); 255 auto MissMatchScore = PreMatch[Miss].Score + matchBonus(P, W, Miss); 256 Score[Match] = (MatchMatchScore > MissMatchScore) 257 ? ScoreInfo{MatchMatchScore, Match} 258 : ScoreInfo{MissMatchScore, Miss}; 259 } 260 } 261 } 262 } 263 264 int FuzzyMatcher::skipPenalty(int W, Action Last) { 265 int S = 0; 266 if (WordRole[W] == Head) // Skipping a segment. 267 S += 1; 268 if (Last == Match) // Non-consecutive match. 269 S += 2; // We'd rather skip a segment than split our match. 270 return S; 271 } 272 273 int FuzzyMatcher::matchBonus(int P, int W, Action Last) { 274 assert(LowPat[P] == LowWord[W]); 275 int S = 1; 276 // Bonus: pattern so far is a (case-insensitive) prefix of the word. 277 if (P == W) // We can't skip pattern characters, so we must have matched all. 278 ++S; 279 // Bonus: case matches, or a Head in the pattern aligns with one in the word. 280 if ((Pat[P] == Word[W] && (CaseSensitive || P == W)) || 281 (PatRole[P] == Head && WordRole[W] == Head)) 282 ++S; 283 // Penalty: matching inside a segment (and previous char wasn't matched). 284 if (WordRole[W] == Tail && P && Last == Miss) 285 S -= 3; 286 // Penalty: a Head in the pattern matches in the middle of a word segment. 287 if (PatRole[P] == Head && WordRole[W] == Tail) 288 --S; 289 // Penalty: matching the first pattern character in the middle of a segment. 290 if (P == 0 && WordRole[W] == Tail) 291 S -= 4; 292 assert(S <= PerfectBonus); 293 return S; 294 } 295 296 llvm::SmallString<256> FuzzyMatcher::dumpLast(llvm::raw_ostream &OS) const { 297 llvm::SmallString<256> Result; 298 OS << "=== Match \"" << StringRef(Word, WordN) << "\" against [" 299 << StringRef(Pat, PatN) << "] ===\n"; 300 if (PatN == 0) { 301 OS << "Pattern is empty: perfect match.\n"; 302 return Result = StringRef(Word, WordN); 303 } 304 if (WordN == 0) { 305 OS << "Word is empty: no match.\n"; 306 return Result; 307 } 308 if (!WordContainsPattern) { 309 OS << "Substring check failed.\n"; 310 return Result; 311 } else if (isAwful(std::max(Scores[PatN][WordN][Match].Score, 312 Scores[PatN][WordN][Miss].Score))) { 313 OS << "Substring check passed, but all matches are forbidden\n"; 314 } 315 if (!CaseSensitive) 316 OS << "Lowercase query, so scoring ignores case\n"; 317 318 // Traverse Matched table backwards to reconstruct the Pattern/Word mapping. 319 // The Score table has cumulative scores, subtracting along this path gives 320 // us the per-letter scores. 321 Action Last = 322 (Scores[PatN][WordN][Match].Score > Scores[PatN][WordN][Miss].Score) 323 ? Match 324 : Miss; 325 int S[MaxWord]; 326 Action A[MaxWord]; 327 for (int W = WordN - 1, P = PatN - 1; W >= 0; --W) { 328 A[W] = Last; 329 const auto &Cell = Scores[P + 1][W + 1][Last]; 330 if (Last == Match) 331 --P; 332 const auto &Prev = Scores[P + 1][W][Cell.Prev]; 333 S[W] = Cell.Score - Prev.Score; 334 Last = Cell.Prev; 335 } 336 for (int I = 0; I < WordN; ++I) { 337 if (A[I] == Match && (I == 0 || A[I - 1] == Miss)) 338 Result.push_back('['); 339 if (A[I] == Miss && I > 0 && A[I - 1] == Match) 340 Result.push_back(']'); 341 Result.push_back(Word[I]); 342 } 343 if (A[WordN - 1] == Match) 344 Result.push_back(']'); 345 346 for (char C : StringRef(Word, WordN)) 347 OS << " " << C << " "; 348 OS << "\n"; 349 for (int I = 0, J = 0; I < WordN; I++) 350 OS << " " << (A[I] == Match ? Pat[J++] : ' ') << " "; 351 OS << "\n"; 352 for (int I = 0; I < WordN; I++) 353 OS << format("%2d ", S[I]); 354 OS << "\n"; 355 356 OS << "\nSegmentation:"; 357 OS << "\n'" << StringRef(Word, WordN) << "'\n "; 358 for (int I = 0; I < WordN; ++I) 359 OS << "?-+ "[static_cast<int>(WordRole[I])]; 360 OS << "\n[" << StringRef(Pat, PatN) << "]\n "; 361 for (int I = 0; I < PatN; ++I) 362 OS << "?-+ "[static_cast<int>(PatRole[I])]; 363 OS << "\n"; 364 365 OS << "\nScoring table (last-Miss, last-Match):\n"; 366 OS << " | "; 367 for (char C : StringRef(Word, WordN)) 368 OS << " " << C << " "; 369 OS << "\n"; 370 OS << "-+----" << std::string(WordN * 4, '-') << "\n"; 371 for (int I = 0; I <= PatN; ++I) { 372 for (Action A : {Miss, Match}) { 373 OS << ((I && A == Miss) ? Pat[I - 1] : ' ') << "|"; 374 for (int J = 0; J <= WordN; ++J) { 375 if (!isAwful(Scores[I][J][A].Score)) 376 OS << format("%3d%c", Scores[I][J][A].Score, 377 Scores[I][J][A].Prev == Match ? '*' : ' '); 378 else 379 OS << " "; 380 } 381 OS << "\n"; 382 } 383 } 384 385 return Result; 386 } 387 388 } // namespace clangd 389 } // namespace clang 390