• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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/parser.h"
18 
19 #include <unordered_map>
20 
21 #include "utils/grammar/parsing/parse-tree.h"
22 #include "utils/grammar/rules-utils.h"
23 #include "utils/grammar/types.h"
24 #include "utils/zlib/zlib.h"
25 #include "utils/zlib/zlib_regex.h"
26 
27 namespace libtextclassifier3::grammar {
28 namespace {
29 
CheckMemoryUsage(const UnsafeArena * arena)30 inline bool CheckMemoryUsage(const UnsafeArena* arena) {
31   // The maximum memory usage for matching.
32   constexpr int kMaxMemoryUsage = 1 << 20;
33   return arena->status().bytes_allocated() <= kMaxMemoryUsage;
34 }
35 
36 // Maps a codepoint to include the token padding if it aligns with a token
37 // start. Whitespace is ignored when symbols are fed to the matcher. Preceding
38 // whitespace is merged to the match start so that tokens and non-terminals
39 // appear next to each other without whitespace. For text or regex annotations,
40 // we therefore merge the whitespace padding to the start if the annotation
41 // starts at a token.
MapCodepointToTokenPaddingIfPresent(const std::unordered_map<CodepointIndex,CodepointIndex> & token_alignment,const int start)42 int MapCodepointToTokenPaddingIfPresent(
43     const std::unordered_map<CodepointIndex, CodepointIndex>& token_alignment,
44     const int start) {
45   const auto it = token_alignment.find(start);
46   if (it != token_alignment.end()) {
47     return it->second;
48   }
49   return start;
50 }
51 
52 }  // namespace
53 
Parser(const UniLib * unilib,const RulesSet * rules)54 Parser::Parser(const UniLib* unilib, const RulesSet* rules)
55     : unilib_(*unilib),
56       rules_(rules),
57       lexer_(unilib),
58       nonterminals_(rules_->nonterminals()),
59       rules_locales_(ParseRulesLocales(rules_)),
60       regex_annotators_(BuildRegexAnnotators()) {}
61 
62 // Uncompresses and build the defined regex annotators.
BuildRegexAnnotators() const63 std::vector<Parser::RegexAnnotator> Parser::BuildRegexAnnotators() const {
64   std::vector<RegexAnnotator> result;
65   if (rules_->regex_annotator() != nullptr) {
66     std::unique_ptr<ZlibDecompressor> decompressor =
67         ZlibDecompressor::Instance();
68     result.reserve(rules_->regex_annotator()->size());
69     for (const RulesSet_::RegexAnnotator* regex_annotator :
70          *rules_->regex_annotator()) {
71       result.push_back(
72           {UncompressMakeRegexPattern(unilib_, regex_annotator->pattern(),
73                                       regex_annotator->compressed_pattern(),
74                                       rules_->lazy_regex_compilation(),
75                                       decompressor.get()),
76            regex_annotator->nonterminal()});
77     }
78   }
79   return result;
80 }
81 
SortedSymbolsForInput(const TextContext & input,UnsafeArena * arena) const82 std::vector<Symbol> Parser::SortedSymbolsForInput(const TextContext& input,
83                                                   UnsafeArena* arena) const {
84   // Whitespace is ignored when symbols are fed to the matcher.
85   // For regex matches and existing text annotations we therefore have to merge
86   // preceding whitespace to the match start so that tokens and non-terminals
87   // appear as next to each other without whitespace. We keep track of real
88   // token starts and precending whitespace in `token_match_start`, so that we
89   // can extend a match's start to include the preceding whitespace.
90   std::unordered_map<CodepointIndex, CodepointIndex> token_match_start;
91   for (int i = input.context_span.first + 1; i < input.context_span.second;
92        i++) {
93     const CodepointIndex token_start = input.tokens[i].start;
94     const CodepointIndex prev_token_end = input.tokens[i - 1].end;
95     if (token_start != prev_token_end) {
96       token_match_start[token_start] = prev_token_end;
97     }
98   }
99 
100   std::vector<Symbol> symbols;
101   CodepointIndex match_offset = input.tokens[input.context_span.first].start;
102 
103   // Add start symbol.
104   if (input.context_span.first == 0 &&
105       nonterminals_->start_nt() != kUnassignedNonterm) {
106     match_offset = 0;
107     symbols.emplace_back(arena->AllocAndInit<ParseTree>(
108         nonterminals_->start_nt(), CodepointSpan{0, 0},
109         /*match_offset=*/0, ParseTree::Type::kDefault));
110   }
111 
112   if (nonterminals_->wordbreak_nt() != kUnassignedNonterm) {
113     symbols.emplace_back(arena->AllocAndInit<ParseTree>(
114         nonterminals_->wordbreak_nt(),
115         CodepointSpan{match_offset, match_offset},
116         /*match_offset=*/match_offset, ParseTree::Type::kDefault));
117   }
118 
119   // Add symbols from tokens.
120   for (int i = input.context_span.first; i < input.context_span.second; i++) {
121     const Token& token = input.tokens[i];
122     lexer_.AppendTokenSymbols(token.value, /*match_offset=*/match_offset,
123                               CodepointSpan{token.start, token.end}, &symbols);
124     match_offset = token.end;
125 
126     // Add word break symbol.
127     if (nonterminals_->wordbreak_nt() != kUnassignedNonterm) {
128       symbols.emplace_back(arena->AllocAndInit<ParseTree>(
129           nonterminals_->wordbreak_nt(),
130           CodepointSpan{match_offset, match_offset},
131           /*match_offset=*/match_offset, ParseTree::Type::kDefault));
132     }
133   }
134 
135   // Add end symbol if used by the grammar.
136   if (input.context_span.second == input.tokens.size() &&
137       nonterminals_->end_nt() != kUnassignedNonterm) {
138     symbols.emplace_back(arena->AllocAndInit<ParseTree>(
139         nonterminals_->end_nt(), CodepointSpan{match_offset, match_offset},
140         /*match_offset=*/match_offset, ParseTree::Type::kDefault));
141   }
142 
143   // Add symbols from the regex annotators.
144   const CodepointIndex context_start =
145       input.tokens[input.context_span.first].start;
146   const CodepointIndex context_end =
147       input.tokens[input.context_span.second - 1].end;
148   for (const RegexAnnotator& regex_annotator : regex_annotators_) {
149     std::unique_ptr<UniLib::RegexMatcher> regex_matcher =
150         regex_annotator.pattern->Matcher(UnicodeText::Substring(
151             input.text, context_start, context_end, /*do_copy=*/false));
152     int status = UniLib::RegexMatcher::kNoError;
153     while (regex_matcher->Find(&status) &&
154            status == UniLib::RegexMatcher::kNoError) {
155       const CodepointSpan span{regex_matcher->Start(0, &status) + context_start,
156                                regex_matcher->End(0, &status) + context_start};
157       symbols.emplace_back(arena->AllocAndInit<ParseTree>(
158           regex_annotator.nonterm, span, /*match_offset=*/
159           MapCodepointToTokenPaddingIfPresent(token_match_start, span.first),
160           ParseTree::Type::kDefault));
161     }
162   }
163 
164   // Add symbols based on annotations.
165   if (auto annotation_nonterminals = nonterminals_->annotation_nt()) {
166     for (const AnnotatedSpan& annotated_span : input.annotations) {
167       const ClassificationResult& classification =
168           annotated_span.classification.front();
169       if (auto entry = annotation_nonterminals->LookupByKey(
170               classification.collection.c_str())) {
171         symbols.emplace_back(arena->AllocAndInit<AnnotationNode>(
172             entry->value(), annotated_span.span, /*match_offset=*/
173             MapCodepointToTokenPaddingIfPresent(token_match_start,
174                                                 annotated_span.span.first),
175             &classification));
176       }
177     }
178   }
179 
180   std::sort(symbols.begin(), symbols.end(),
181             [](const Symbol& a, const Symbol& b) {
182               // Sort by increasing (end, start) position to guarantee the
183               // matcher requirement that the tokens are fed in non-decreasing
184               // end position order.
185               return std::tie(a.codepoint_span.second, a.codepoint_span.first) <
186                      std::tie(b.codepoint_span.second, b.codepoint_span.first);
187             });
188 
189   return symbols;
190 }
191 
EmitSymbol(const Symbol & symbol,UnsafeArena * arena,Matcher * matcher) const192 void Parser::EmitSymbol(const Symbol& symbol, UnsafeArena* arena,
193                         Matcher* matcher) const {
194   if (!CheckMemoryUsage(arena)) {
195     return;
196   }
197   switch (symbol.type) {
198     case Symbol::Type::TYPE_PARSE_TREE: {
199       // Just emit the parse tree.
200       matcher->AddParseTree(symbol.parse_tree);
201       return;
202     }
203     case Symbol::Type::TYPE_DIGITS: {
204       // Emit <digits> if used by the rules.
205       if (nonterminals_->digits_nt() != kUnassignedNonterm) {
206         matcher->AddParseTree(arena->AllocAndInit<ParseTree>(
207             nonterminals_->digits_nt(), symbol.codepoint_span,
208             symbol.match_offset, ParseTree::Type::kDefault));
209       }
210 
211       // Emit <n_digits> if used by the rules.
212       if (nonterminals_->n_digits_nt() != nullptr) {
213         const int num_digits =
214             symbol.codepoint_span.second - symbol.codepoint_span.first;
215         if (num_digits <= nonterminals_->n_digits_nt()->size()) {
216           const Nonterm n_digits_nt =
217               nonterminals_->n_digits_nt()->Get(num_digits - 1);
218           if (n_digits_nt != kUnassignedNonterm) {
219             matcher->AddParseTree(arena->AllocAndInit<ParseTree>(
220                 nonterminals_->n_digits_nt()->Get(num_digits - 1),
221                 symbol.codepoint_span, symbol.match_offset,
222                 ParseTree::Type::kDefault));
223           }
224         }
225       }
226       break;
227     }
228     case Symbol::Type::TYPE_TERM: {
229       // Emit <uppercase_token> if used by the rules.
230       if (nonterminals_->uppercase_token_nt() != 0 &&
231           unilib_.IsUpperText(
232               UTF8ToUnicodeText(symbol.lexeme, /*do_copy=*/false))) {
233         matcher->AddParseTree(arena->AllocAndInit<ParseTree>(
234             nonterminals_->uppercase_token_nt(), symbol.codepoint_span,
235             symbol.match_offset, ParseTree::Type::kDefault));
236       }
237       break;
238     }
239     default:
240       break;
241   }
242 
243   // Emit the token as terminal.
244   matcher->AddTerminal(symbol.codepoint_span, symbol.match_offset,
245                        symbol.lexeme);
246 
247   // Emit <token> if used by rules.
248   matcher->AddParseTree(arena->AllocAndInit<ParseTree>(
249       nonterminals_->token_nt(), symbol.codepoint_span, symbol.match_offset,
250       ParseTree::Type::kDefault));
251 }
252 
253 // Parses an input text and returns the root rule derivations.
Parse(const TextContext & input,UnsafeArena * arena) const254 std::vector<Derivation> Parser::Parse(const TextContext& input,
255                                       UnsafeArena* arena) const {
256   // Check the tokens, input can be non-empty (whitespace) but have no tokens.
257   if (input.tokens.empty()) {
258     return {};
259   }
260 
261   // Select locale matching rules.
262   std::vector<const RulesSet_::Rules*> locale_rules =
263       SelectLocaleMatchingShards(rules_, rules_locales_, input.locales);
264 
265   if (locale_rules.empty()) {
266     // Nothing to do.
267     return {};
268   }
269 
270   Matcher matcher(&unilib_, rules_, locale_rules, arena);
271   for (const Symbol& symbol : SortedSymbolsForInput(input, arena)) {
272     EmitSymbol(symbol, arena, &matcher);
273   }
274   matcher.Finish();
275   return matcher.chart().derivations();
276 }
277 
278 }  // namespace libtextclassifier3::grammar
279