1 /*
2 * Copyright (C) 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include "utils/grammar/parsing/matcher.h"
18
19 #include <iostream>
20 #include <limits>
21
22 #include "utils/base/endian.h"
23 #include "utils/base/logging.h"
24 #include "utils/base/macros.h"
25 #include "utils/grammar/types.h"
26 #include "utils/strings/utf8.h"
27
28 namespace libtextclassifier3::grammar {
29 namespace {
30
31 // Iterator that just enumerates the bytes in a utf8 text.
32 struct ByteIterator {
ByteIteratorlibtextclassifier3::grammar::__anon8d12a22f0111::ByteIterator33 explicit ByteIterator(StringPiece text)
34 : data(text.data()), end(text.data() + text.size()) {}
35
Nextlibtextclassifier3::grammar::__anon8d12a22f0111::ByteIterator36 inline char Next() {
37 TC3_DCHECK(HasNext());
38 const char c = data[0];
39 data++;
40 return c;
41 }
HasNextlibtextclassifier3::grammar::__anon8d12a22f0111::ByteIterator42 inline bool HasNext() const { return data < end; }
43
44 const char* data;
45 const char* end;
46 };
47
48 // Iterator that lowercases a utf8 string on the fly and enumerates the bytes.
49 struct LowercasingByteIterator {
LowercasingByteIteratorlibtextclassifier3::grammar::__anon8d12a22f0111::LowercasingByteIterator50 LowercasingByteIterator(const UniLib* unilib, StringPiece text)
51 : unilib(*unilib),
52 data(text.data()),
53 end(text.data() + text.size()),
54 buffer_pos(0),
55 buffer_size(0) {}
56
Nextlibtextclassifier3::grammar::__anon8d12a22f0111::LowercasingByteIterator57 inline char Next() {
58 // Queue next character.
59 if (buffer_pos >= buffer_size) {
60 buffer_pos = 0;
61
62 // Lower-case the next character. The character and its lower-cased
63 // counterpart may be represented with a different number of bytes in
64 // utf8.
65 buffer_size =
66 ValidRuneToChar(unilib.ToLower(ValidCharToRune(data)), buffer);
67 data += GetNumBytesForUTF8Char(data);
68 }
69 TC3_DCHECK_LT(buffer_pos, buffer_size);
70 return buffer[buffer_pos++];
71 }
72
HasNextlibtextclassifier3::grammar::__anon8d12a22f0111::LowercasingByteIterator73 inline bool HasNext() const {
74 // Either we are not at the end of the data or didn't consume all bytes of
75 // the current character.
76 return (data < end || buffer_pos < buffer_size);
77 }
78
79 const UniLib& unilib;
80 const char* data;
81 const char* end;
82
83 // Each unicode codepoint can have up to 4 utf8 encoding bytes.
84 char buffer[4];
85 int buffer_pos;
86 int buffer_size;
87 };
88
89 // Searches a terminal match within a sorted table of terminals.
90 // Using `LowercasingByteIterator` allows to lower-case the query string on the
91 // fly.
92 template <typename T>
FindTerminal(T input_iterator,const char * strings,const uint32 * offsets,const int num_terminals,int * terminal_index)93 const char* FindTerminal(T input_iterator, const char* strings,
94 const uint32* offsets, const int num_terminals,
95 int* terminal_index) {
96 int left = 0;
97 int right = num_terminals;
98 int span_size = right - left;
99 int match_length = 0;
100
101 // Loop invariant:
102 // At the ith iteration, all strings in the range `left` ... `right` match the
103 // input on the first `match_length` characters.
104 while (true) {
105 const unsigned char c =
106 static_cast<const unsigned char>(input_iterator.Next());
107
108 // We find the possible range of strings in `left` ... `right` matching the
109 // `match_length` + 1 character with two binary searches:
110 // 1) `lower_bound` to find the start of the range of matching strings.
111 // 2) `upper_bound` to find the non-inclusive end of the range.
112 left =
113 (std::lower_bound(
114 offsets + left, offsets + right, c,
115 [strings, match_length](uint32 string_offset, uint32 c) -> bool {
116 return static_cast<unsigned char>(
117 strings[string_offset + match_length]) <
118 LittleEndian::ToHost32(c);
119 }) -
120 offsets);
121 right =
122 (std::upper_bound(
123 offsets + left, offsets + right, c,
124 [strings, match_length](uint32 c, uint32 string_offset) -> bool {
125 return LittleEndian::ToHost32(c) <
126 static_cast<unsigned char>(
127 strings[string_offset + match_length]);
128 }) -
129 offsets);
130 span_size = right - left;
131 if (span_size <= 0) {
132 return nullptr;
133 }
134 ++match_length;
135
136 // By the loop invariant and due to the fact that the strings are sorted,
137 // a matching string will be at `left` now.
138 if (!input_iterator.HasNext()) {
139 const int string_offset = LittleEndian::ToHost32(offsets[left]);
140 if (strings[string_offset + match_length] == 0) {
141 *terminal_index = left;
142 return &strings[string_offset];
143 }
144 return nullptr;
145 }
146 }
147
148 // No match found.
149 return nullptr;
150 }
151
152 // Finds terminal matches in the terminal rules hash tables.
153 // In case a match is found, `terminal` will be set to point into the
154 // terminals string pool.
155 template <typename T>
FindTerminalMatches(T input_iterator,const RulesSet * rules_set,const RulesSet_::Rules_::TerminalRulesMap * terminal_rules,StringPiece * terminal)156 const RulesSet_::LhsSet* FindTerminalMatches(
157 T input_iterator, const RulesSet* rules_set,
158 const RulesSet_::Rules_::TerminalRulesMap* terminal_rules,
159 StringPiece* terminal) {
160 const int terminal_size = terminal->size();
161 if (terminal_size < terminal_rules->min_terminal_length() ||
162 terminal_size > terminal_rules->max_terminal_length()) {
163 return nullptr;
164 }
165 int terminal_index;
166 if (const char* terminal_match = FindTerminal(
167 input_iterator, rules_set->terminals()->data(),
168 terminal_rules->terminal_offsets()->data(),
169 terminal_rules->terminal_offsets()->size(), &terminal_index)) {
170 *terminal = StringPiece(terminal_match, terminal->length());
171 return rules_set->lhs_set()->Get(
172 terminal_rules->lhs_set_index()->Get(terminal_index));
173 }
174 return nullptr;
175 }
176
177 // Finds unary rules matches.
FindUnaryRulesMatches(const RulesSet * rules_set,const RulesSet_::Rules * rules,const Nonterm nonterminal)178 const RulesSet_::LhsSet* FindUnaryRulesMatches(const RulesSet* rules_set,
179 const RulesSet_::Rules* rules,
180 const Nonterm nonterminal) {
181 if (!rules->unary_rules()) {
182 return nullptr;
183 }
184 if (const RulesSet_::Rules_::UnaryRulesEntry* entry =
185 rules->unary_rules()->LookupByKey(nonterminal)) {
186 return rules_set->lhs_set()->Get(entry->value());
187 }
188 return nullptr;
189 }
190
191 // Finds binary rules matches.
FindBinaryRulesMatches(const RulesSet * rules_set,const RulesSet_::Rules * rules,const TwoNonterms nonterminals)192 const RulesSet_::LhsSet* FindBinaryRulesMatches(
193 const RulesSet* rules_set, const RulesSet_::Rules* rules,
194 const TwoNonterms nonterminals) {
195 if (!rules->binary_rules()) {
196 return nullptr;
197 }
198
199 // Lookup in rules hash table.
200 const uint32 bucket_index =
201 BinaryRuleHasher()(nonterminals) % rules->binary_rules()->size();
202
203 // Get hash table bucket.
204 if (const RulesSet_::Rules_::BinaryRuleTableBucket* bucket =
205 rules->binary_rules()->Get(bucket_index)) {
206 if (bucket->rules() == nullptr) {
207 return nullptr;
208 }
209
210 // Check all entries in the chain.
211 for (const RulesSet_::Rules_::BinaryRule* rule : *bucket->rules()) {
212 if (rule->rhs_first() == nonterminals.first &&
213 rule->rhs_second() == nonterminals.second) {
214 return rules_set->lhs_set()->Get(rule->lhs_set_index());
215 }
216 }
217 }
218
219 return nullptr;
220 }
221
GetLhs(const RulesSet * rules_set,const int lhs_entry,Nonterm * nonterminal,CallbackId * callback,int64 * param,int8 * max_whitespace_gap)222 inline void GetLhs(const RulesSet* rules_set, const int lhs_entry,
223 Nonterm* nonterminal, CallbackId* callback, int64* param,
224 int8* max_whitespace_gap) {
225 if (lhs_entry > 0) {
226 // Direct encoding of the nonterminal.
227 *nonterminal = lhs_entry;
228 *callback = kNoCallback;
229 *param = 0;
230 *max_whitespace_gap = -1;
231 } else {
232 const RulesSet_::Lhs* lhs = rules_set->lhs()->Get(-lhs_entry);
233 *nonterminal = lhs->nonterminal();
234 *callback = lhs->callback_id();
235 *param = lhs->callback_param();
236 *max_whitespace_gap = lhs->max_whitespace_gap();
237 }
238 }
239
240 } // namespace
241
Finish()242 void Matcher::Finish() {
243 // Check any pending items.
244 ProcessPendingExclusionMatches();
245 }
246
QueueForProcessing(ParseTree * item)247 void Matcher::QueueForProcessing(ParseTree* item) {
248 // Push element to the front.
249 item->next = pending_items_;
250 pending_items_ = item;
251 }
252
QueueForPostCheck(ExclusionNode * item)253 void Matcher::QueueForPostCheck(ExclusionNode* item) {
254 // Push element to the front.
255 item->next = pending_exclusion_items_;
256 pending_exclusion_items_ = item;
257 }
258
AddTerminal(const CodepointSpan codepoint_span,const int match_offset,StringPiece terminal)259 void Matcher::AddTerminal(const CodepointSpan codepoint_span,
260 const int match_offset, StringPiece terminal) {
261 TC3_CHECK_GE(codepoint_span.second, last_end_);
262
263 // Finish any pending post-checks.
264 if (codepoint_span.second > last_end_) {
265 ProcessPendingExclusionMatches();
266 }
267
268 last_end_ = codepoint_span.second;
269 for (const RulesSet_::Rules* shard : rules_shards_) {
270 // Try case-sensitive matches.
271 if (const RulesSet_::LhsSet* lhs_set =
272 FindTerminalMatches(ByteIterator(terminal), rules_,
273 shard->terminal_rules(), &terminal)) {
274 // `terminal` points now into the rules string pool, providing a
275 // stable reference.
276 ExecuteLhsSet(
277 codepoint_span, match_offset,
278 /*whitespace_gap=*/(codepoint_span.first - match_offset),
279 [terminal](ParseTree* parse_tree) {
280 parse_tree->terminal = terminal.data();
281 parse_tree->rhs2 = nullptr;
282 },
283 lhs_set);
284 }
285
286 // Try case-insensitive matches.
287 if (const RulesSet_::LhsSet* lhs_set = FindTerminalMatches(
288 LowercasingByteIterator(&unilib_, terminal), rules_,
289 shard->lowercase_terminal_rules(), &terminal)) {
290 // `terminal` points now into the rules string pool, providing a
291 // stable reference.
292 ExecuteLhsSet(
293 codepoint_span, match_offset,
294 /*whitespace_gap=*/(codepoint_span.first - match_offset),
295 [terminal](ParseTree* parse_tree) {
296 parse_tree->terminal = terminal.data();
297 parse_tree->rhs2 = nullptr;
298 },
299 lhs_set);
300 }
301 }
302 ProcessPendingSet();
303 }
304
AddParseTree(ParseTree * parse_tree)305 void Matcher::AddParseTree(ParseTree* parse_tree) {
306 TC3_CHECK_GE(parse_tree->codepoint_span.second, last_end_);
307
308 // Finish any pending post-checks.
309 if (parse_tree->codepoint_span.second > last_end_) {
310 ProcessPendingExclusionMatches();
311 }
312
313 last_end_ = parse_tree->codepoint_span.second;
314 QueueForProcessing(parse_tree);
315 ProcessPendingSet();
316 }
317
ExecuteLhsSet(const CodepointSpan codepoint_span,const int match_offset_bytes,const int whitespace_gap,const std::function<void (ParseTree *)> & initializer_fn,const RulesSet_::LhsSet * lhs_set)318 void Matcher::ExecuteLhsSet(
319 const CodepointSpan codepoint_span, const int match_offset_bytes,
320 const int whitespace_gap,
321 const std::function<void(ParseTree*)>& initializer_fn,
322 const RulesSet_::LhsSet* lhs_set) {
323 TC3_CHECK(lhs_set);
324 ParseTree* parse_tree = nullptr;
325 Nonterm prev_lhs = kUnassignedNonterm;
326 for (const int32 lhs_entry : *lhs_set->lhs()) {
327 Nonterm lhs;
328 CallbackId callback_id;
329 int64 callback_param;
330 int8 max_whitespace_gap;
331 GetLhs(rules_, lhs_entry, &lhs, &callback_id, &callback_param,
332 &max_whitespace_gap);
333
334 // Check that the allowed whitespace gap limit is followed.
335 if (max_whitespace_gap >= 0 && whitespace_gap > max_whitespace_gap) {
336 continue;
337 }
338
339 // Handle callbacks.
340 switch (static_cast<DefaultCallback>(callback_id)) {
341 case DefaultCallback::kAssertion: {
342 AssertionNode* assertion_node = arena_->AllocAndInit<AssertionNode>(
343 lhs, codepoint_span, match_offset_bytes,
344 /*negative=*/(callback_param != 0));
345 initializer_fn(assertion_node);
346 QueueForProcessing(assertion_node);
347 continue;
348 }
349 case DefaultCallback::kMapping: {
350 MappingNode* mapping_node = arena_->AllocAndInit<MappingNode>(
351 lhs, codepoint_span, match_offset_bytes, /*id=*/callback_param);
352 initializer_fn(mapping_node);
353 QueueForProcessing(mapping_node);
354 continue;
355 }
356 case DefaultCallback::kExclusion: {
357 // We can only check the exclusion once all matches up to this position
358 // have been processed. Schedule and post check later.
359 ExclusionNode* exclusion_node = arena_->AllocAndInit<ExclusionNode>(
360 lhs, codepoint_span, match_offset_bytes,
361 /*exclusion_nonterm=*/callback_param);
362 initializer_fn(exclusion_node);
363 QueueForPostCheck(exclusion_node);
364 continue;
365 }
366 case DefaultCallback::kSemanticExpression: {
367 SemanticExpressionNode* expression_node =
368 arena_->AllocAndInit<SemanticExpressionNode>(
369 lhs, codepoint_span, match_offset_bytes,
370 /*expression=*/
371 rules_->semantic_expression()->Get(callback_param));
372 initializer_fn(expression_node);
373 QueueForProcessing(expression_node);
374 continue;
375 }
376 default:
377 break;
378 }
379
380 if (prev_lhs != lhs) {
381 prev_lhs = lhs;
382 parse_tree = arena_->AllocAndInit<ParseTree>(
383 lhs, codepoint_span, match_offset_bytes, ParseTree::Type::kDefault);
384 initializer_fn(parse_tree);
385 QueueForProcessing(parse_tree);
386 }
387
388 if (static_cast<DefaultCallback>(callback_id) ==
389 DefaultCallback::kRootRule) {
390 chart_.AddDerivation(Derivation{parse_tree, /*rule_id=*/callback_param});
391 }
392 }
393 }
394
ProcessPendingSet()395 void Matcher::ProcessPendingSet() {
396 while (pending_items_) {
397 // Process.
398 ParseTree* item = pending_items_;
399 pending_items_ = pending_items_->next;
400
401 // Add it to the chart.
402 chart_.Add(item);
403
404 // Check unary rules that trigger.
405 for (const RulesSet_::Rules* shard : rules_shards_) {
406 if (const RulesSet_::LhsSet* lhs_set =
407 FindUnaryRulesMatches(rules_, shard, item->lhs)) {
408 ExecuteLhsSet(
409 item->codepoint_span, item->match_offset,
410 /*whitespace_gap=*/
411 (item->codepoint_span.first - item->match_offset),
412 [item](ParseTree* parse_tree) {
413 parse_tree->rhs1 = nullptr;
414 parse_tree->rhs2 = item;
415 },
416 lhs_set);
417 }
418 }
419
420 // Check binary rules that trigger.
421 // Lookup by begin.
422 for (Chart<>::Iterator it = chart_.MatchesEndingAt(item->match_offset);
423 !it.Done(); it.Next()) {
424 const ParseTree* prev = it.Item();
425 for (const RulesSet_::Rules* shard : rules_shards_) {
426 if (const RulesSet_::LhsSet* lhs_set =
427 FindBinaryRulesMatches(rules_, shard, {prev->lhs, item->lhs})) {
428 ExecuteLhsSet(
429 /*codepoint_span=*/
430 {prev->codepoint_span.first, item->codepoint_span.second},
431 prev->match_offset,
432 /*whitespace_gap=*/
433 (item->codepoint_span.first -
434 item->match_offset), // Whitespace gap is the gap
435 // between the two parts.
436 [prev, item](ParseTree* parse_tree) {
437 parse_tree->rhs1 = prev;
438 parse_tree->rhs2 = item;
439 },
440 lhs_set);
441 }
442 }
443 }
444 }
445 }
446
ProcessPendingExclusionMatches()447 void Matcher::ProcessPendingExclusionMatches() {
448 while (pending_exclusion_items_) {
449 ExclusionNode* item = pending_exclusion_items_;
450 pending_exclusion_items_ = static_cast<ExclusionNode*>(item->next);
451
452 // Check that the exclusion condition is fulfilled.
453 if (!chart_.HasMatch(item->exclusion_nonterm, item->codepoint_span)) {
454 AddParseTree(item);
455 }
456 }
457 }
458
459 } // namespace libtextclassifier3::grammar
460