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