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