meta_language/grammar/inference/
lexical.rs1use std::collections::{BTreeMap, BTreeSet};
4use std::fmt::Write as _;
5
6use crate::grammar::{CharClassItem, GrammarExpr, GrammarRule, RuleKind};
7use crate::source::ByteRange;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub enum CharCategory {
12 Letter,
14 Digit,
16 Whitespace,
18 Delimiter,
20 Punctuation,
22 Other,
24}
25
26#[must_use]
28pub fn categorise(value: char) -> CharCategory {
29 if value.is_whitespace() {
30 CharCategory::Whitespace
31 } else if is_delimiter(value) {
32 CharCategory::Delimiter
33 } else if value.is_alphabetic() {
34 CharCategory::Letter
35 } else if value.is_numeric() {
36 CharCategory::Digit
37 } else if value.is_ascii_punctuation() {
38 CharCategory::Punctuation
39 } else {
40 CharCategory::Other
41 }
42}
43
44#[derive(Clone, Debug, PartialEq, Eq)]
46pub struct Token {
47 pub text: String,
49 pub category: CharCategory,
51 pub span: ByteRange,
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub struct LexicalConfig {
58 pub max_closed_forms: usize,
60}
61
62impl Default for LexicalConfig {
63 fn default() -> Self {
64 Self {
65 max_closed_forms: 12,
66 }
67 }
68}
69
70#[derive(Clone, Debug, PartialEq, Eq)]
72pub struct LexicalModel {
73 pub alphabet: Vec<char>,
75 pub classes: Vec<GrammarRule>,
77 pub config: LexicalConfig,
79}
80
81impl LexicalModel {
82 #[must_use]
84 pub fn tokenize(&self, text: &str) -> Vec<Token> {
85 tokenize_text(text, self.config)
86 }
87}
88
89#[must_use]
91pub fn infer_lexical_classes(corpus: &[&str], config: &LexicalConfig) -> LexicalModel {
92 let mut alphabet = BTreeSet::new();
93 let mut forms_by_category = BTreeMap::<CharCategory, BTreeMap<String, usize>>::new();
94
95 for text in corpus {
96 alphabet.extend(text.chars());
97
98 for token in tokenize_text(text, *config) {
99 *forms_by_category
100 .entry(token.category)
101 .or_default()
102 .entry(token.text)
103 .or_default() += 1;
104 }
105 }
106
107 LexicalModel {
108 alphabet: alphabet.into_iter().collect(),
109 classes: infer_classes(&forms_by_category, *config),
110 config: *config,
111 }
112}
113
114fn tokenize_text(text: &str, _config: LexicalConfig) -> Vec<Token> {
115 let mut tokens = Vec::new();
116 let mut token_start = 0;
117 let mut token_end = 0;
118 let mut token_category = None;
119
120 for (index, value) in text.char_indices() {
121 let category = categorise(value);
122 let value_end = index + value.len_utf8();
123
124 let Some(current_category) = token_category else {
125 token_start = index;
126 token_end = value_end;
127 token_category = Some(category);
128 continue;
129 };
130
131 if continues_token(current_category, category) {
132 token_end = value_end;
133 } else {
134 tokens.push(Token {
135 text: text[token_start..token_end].to_string(),
136 category: current_category,
137 span: ByteRange::new(token_start, token_end),
138 });
139 token_start = index;
140 token_end = value_end;
141 token_category = Some(category);
142 }
143 }
144
145 if let Some(category) = token_category {
146 tokens.push(Token {
147 text: text[token_start..token_end].to_string(),
148 category,
149 span: ByteRange::new(token_start, token_end),
150 });
151 }
152
153 tokens
154}
155
156fn continues_token(current: CharCategory, next: CharCategory) -> bool {
157 if is_atomic(current) || is_atomic(next) {
158 return false;
159 }
160
161 current == next || (current == CharCategory::Letter && next == CharCategory::Digit)
162}
163
164const fn is_atomic(category: CharCategory) -> bool {
165 matches!(
166 category,
167 CharCategory::Delimiter | CharCategory::Punctuation
168 )
169}
170
171const fn is_delimiter(value: char) -> bool {
172 matches!(value, '(' | ')' | '[' | ']' | '{' | '}' | '\'' | '"' | '`')
173}
174
175fn infer_classes(
176 forms_by_category: &BTreeMap<CharCategory, BTreeMap<String, usize>>,
177 config: LexicalConfig,
178) -> Vec<GrammarRule> {
179 let mut classes = Vec::new();
180
181 for (category, forms) in forms_by_category {
182 match category {
183 CharCategory::Delimiter | CharCategory::Punctuation => {
184 classes.extend(forms.keys().map(|form| literal_rule(form)));
185 }
186 CharCategory::Digit | CharCategory::Whitespace => {
187 classes.push(open_rule(*category, forms.keys().map(String::as_str)));
188 }
189 CharCategory::Letter | CharCategory::Other => {
190 classes.extend(infer_mixed_category(*category, forms, config));
191 }
192 }
193 }
194
195 classes
196}
197
198fn infer_mixed_category(
199 category: CharCategory,
200 forms: &BTreeMap<String, usize>,
201 config: LexicalConfig,
202) -> Vec<GrammarRule> {
203 let has_repeated_forms = forms.values().any(|count| *count > 1);
204 let has_singleton_forms = forms.values().any(|count| *count == 1);
205
206 if forms.len() <= config.max_closed_forms && !(has_repeated_forms && has_singleton_forms) {
207 return forms.keys().map(|form| literal_rule(form)).collect();
208 }
209
210 let mut rules = Vec::new();
211 let mut open_forms = Vec::new();
212 let mut closed_forms = 0;
213
214 for (form, count) in forms {
215 if *count > 1 && closed_forms < config.max_closed_forms {
216 rules.push(literal_rule(form));
217 closed_forms += 1;
218 } else {
219 open_forms.push(form.as_str());
220 }
221 }
222
223 if !open_forms.is_empty() {
224 rules.push(open_rule(category, open_forms));
225 }
226
227 rules
228}
229
230fn literal_rule(form: &str) -> GrammarRule {
231 GrammarRule::new(literal_rule_name(form), GrammarExpr::terminal(form))
232 .with_kind(RuleKind::Token)
233}
234
235fn literal_rule_name(form: &str) -> String {
236 let mut name = String::from("literal");
237
238 for value in form.chars() {
239 name.push('_');
240 if value.is_ascii_alphanumeric() {
241 name.push(value.to_ascii_lowercase());
242 } else {
243 name.push('u');
244 write!(name, "{:04x}", u32::from(value)).expect("writing to a String cannot fail");
245 }
246 }
247
248 name
249}
250
251fn open_rule<'a>(category: CharCategory, forms: impl IntoIterator<Item = &'a str>) -> GrammarRule {
252 GrammarRule::new(open_rule_name(category), open_expr(category, forms))
253 .with_kind(RuleKind::Token)
254}
255
256const fn open_rule_name(category: CharCategory) -> &'static str {
257 match category {
258 CharCategory::Letter => "identifier",
259 CharCategory::Digit => "integer",
260 CharCategory::Whitespace => "whitespace",
261 CharCategory::Delimiter => "delimiter",
262 CharCategory::Punctuation => "punctuation",
263 CharCategory::Other => "other",
264 }
265}
266
267fn open_expr<'a>(category: CharCategory, forms: impl IntoIterator<Item = &'a str>) -> GrammarExpr {
268 let forms = forms.into_iter().collect::<Vec<_>>();
269
270 match category {
271 CharCategory::Letter => identifier_expr(&forms),
272 CharCategory::Digit if all_ascii_digits(&forms) => {
273 GrammarExpr::one_or_more(GrammarExpr::char_range('0', '9'))
274 }
275 CharCategory::Digit | CharCategory::Whitespace | CharCategory::Other => {
276 GrammarExpr::one_or_more(char_set_expr(&all_chars(&forms)))
277 }
278 CharCategory::Delimiter | CharCategory::Punctuation => {
279 GrammarExpr::choice(false, forms.into_iter().map(GrammarExpr::terminal))
280 }
281 }
282}
283
284fn identifier_expr(forms: &[&str]) -> GrammarExpr {
285 let mut first_chars = BTreeSet::new();
286 let mut rest_chars = BTreeSet::new();
287
288 for form in forms {
289 let mut chars = form.chars();
290 if let Some(first) = chars.next() {
291 first_chars.insert(first);
292 rest_chars.extend(chars);
293 }
294 }
295
296 let first = char_set_expr(&first_chars);
297 if rest_chars.is_empty() {
298 first
299 } else {
300 GrammarExpr::sequence([first, GrammarExpr::zero_or_more(char_set_expr(&rest_chars))])
301 }
302}
303
304fn all_ascii_digits(forms: &[&str]) -> bool {
305 forms
306 .iter()
307 .flat_map(|form| form.chars())
308 .all(|value| value.is_ascii_digit())
309}
310
311fn all_chars(forms: &[&str]) -> BTreeSet<char> {
312 forms.iter().flat_map(|form| form.chars()).collect()
313}
314
315fn char_set_expr(chars: &BTreeSet<char>) -> GrammarExpr {
316 GrammarExpr::char_class(false, char_class_items(chars))
317}
318
319fn char_class_items(chars: &BTreeSet<char>) -> Vec<CharClassItem> {
320 let mut items = Vec::new();
321 let mut chars = chars.iter().copied();
322 let Some(mut range_start) = chars.next() else {
323 return items;
324 };
325 let mut range_end = range_start;
326
327 for value in chars {
328 if u32::from(value) != u32::from(range_end).saturating_add(1) {
329 push_char_class_item(&mut items, range_start, range_end);
330 range_start = value;
331 }
332 range_end = value;
333 }
334
335 push_char_class_item(&mut items, range_start, range_end);
336 items
337}
338
339fn push_char_class_item(items: &mut Vec<CharClassItem>, start: char, end: char) {
340 if start == end {
341 items.push(CharClassItem::char(start));
342 } else {
343 items.push(CharClassItem::range(start, end));
344 }
345}