-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathtoken.rs
More file actions
472 lines (417 loc) · 17 KB
/
Copy pathtoken.rs
File metadata and controls
472 lines (417 loc) · 17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
use crate::codegen::c::CLowerer;
use crate::codegen::token::{Token, TokenInfo, TokenStyle, Tokens};
use crate::util::name::next_name;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::iter;
pub const LEFT_PAREN: Token<'static> = Token::new(Cow::Borrowed("("));
pub const RIGHT_PAREN: Token<'static> = Token::new(Cow::Borrowed(")"));
pub const LEFT_SQUIGGLE: Token<'static> = Token::new(Cow::Borrowed("{"));
pub const RIGHT_SQUIGGLE: Token<'static> = Token::new(Cow::Borrowed("}"));
pub const LEFT_BRACKET: Token<'static> = Token::new(Cow::Borrowed("["));
pub const RIGHT_BRACKET: Token<'static> = Token::new(Cow::Borrowed("]"));
pub const SEMI: Token<'static> = Token::new(Cow::Borrowed(";"));
pub const NEWLINE_REQUIRED: Token<'static> = Token::new(Cow::Borrowed("\n"));
pub const INDENT: Token<'static> = Token::new_fancy(Cow::Borrowed(" "));
pub const NEWLINE: Token<'static> = Token::new_fancy(Cow::Borrowed("\n"));
impl TokenInfo for CLowerer {
fn needs_space_between<'a>(&self, left: &Token<'a>, right: &Token<'a>) -> bool {
if left.style == TokenStyle::Marker && right.style == TokenStyle::Marker {
return false;
}
let Some(left_text) = &left.text else {
return false;
};
let Some(right_text) = &right.text else {
return false;
};
if left_text.is_empty() || right_text.is_empty() {
return false;
}
let mut left_char = left_text.chars().last().unwrap();
let mut right_char = right_text.chars().next().unwrap();
// Treat markers as identifiers to be conservative about
// inserting spaces.
if left.style == TokenStyle::Marker {
left_char = 'a';
}
if right.style == TokenStyle::Marker {
right_char = 'a';
}
// Words must be separated.
// For example, int main, return 0, variable1 variable2, 123 456
if is_ident_char(left_char) && is_ident_char(right_char) {
return true;
}
// Spaces are already allowed between operators,
// and excluding a space here creates a different meaning.
if is_punct_char(left_char) && is_punct_char(right_char) {
// Spaces aren't needed if they don't form compound operators.
return matches!(
(left_char, right_char),
('/', '*')
| ('/', '/')
| ('+', '+')
| ('-', '-')
| ('-', '>')
| ('<', '<')
| ('>', '>')
| ('=', '=')
| ('<', '=')
| ('>', '=')
| ('!', '=')
| ('&', '&')
| ('|', '|')
| ('+', '=')
| ('-', '=')
| ('*', '=')
| ('/', '=')
| ('%', '=')
| ('&', '=')
| ('|', '=')
| ('^', '=')
);
}
// Literal prefixes.
if (right_char == '"' || right_char == '\'') && (matches!(left_char, 'L' | 'u' | 'U')) {
return true;
}
false
}
}
/// Returns true for [a-zA-Z0-9_].
/// Used to detect Identifiers, Keywords, and Numbers.
fn is_ident_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
/// Returns true for C operator/separator symbols.
fn is_punct_char(c: char) -> bool {
"!%^&*-+=|~<>.?/:".contains(c)
}
/// Escapes a string for use in a C string literal.
pub fn escape_string(s: &str) -> String {
let mut output = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => output.push_str("\\\""),
c => append_escape(&mut output, c),
}
}
output
}
/// Escapes a char for use in a C char literal.
pub fn escape_char(s: &str) -> String {
let mut output = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\'' => output.push_str("\\'"),
c => append_escape(&mut output, c),
}
}
output
}
/// Generic escape for strings and chars.
/// Doesn't include escaping for ' / ".
pub fn append_escape(append: &mut String, c: char) {
let add = match c {
'\\' => "\\\\",
'\n' => "\\n",
'\r' => "\\r",
'\t' => "\\t",
'\0' => "\\0",
c => {
if c.is_control() {
if c.is_ascii() && (c as u32) <= 0o777 {
// \nnn in octal
&format!("\\{:03o}", c as u32)
} else {
// \uhhhh in hex
&format!("\\u{:04x}", c as u32)
}
} else {
append.push(c);
return;
}
}
};
append.push_str(add);
}
/// The full cost of a define line: `#define {name} {text}\n`.
///
/// If the value contains newlines, each one requires an extra
/// backslash, adding to the cost.
fn define_line_cost(text: &str, replace_len: usize) -> i32 {
let newline_cost = text.bytes().filter(|&b| b == b'\n').count();
("#define ".len() + replace_len + " ".len() + text.len() + newline_cost + "\n".len()) as i32
}
/// How many characters are saved by replacing a token with text `text`,
/// appearing `count` times, with a define.
fn define_savings(text: &str, count: usize, replace_len: usize) -> i32 {
let saved_per_use = text.len() as i32 - replace_len as i32;
let line_cost = define_line_cost(text, replace_len);
saved_per_use * count as i32 - line_cost
}
/// Merges two token texts into a single string, inserting a space
/// between them if required by the token info.
fn merge_token_text(info: &impl TokenInfo, left: &Token, right: &Token) -> String {
let left_text = left.text.as_ref().expect("Token text is required");
let right_text = right.text.as_ref().expect("Token text is required");
let space = if info.needs_space_between(left, right) {
" "
} else {
""
};
format!("{}{}{}", left_text, space, right_text)
}
/// Returns `(left_text, right_text, merged_text)` for the
/// pair at `body[i]` and `body[i+1]`, or `None` if they can't be
/// merged.
fn pair_at<'a>(
info: &impl TokenInfo,
body: &Tokens<'a>,
i: usize,
) -> Option<(Cow<'a, str>, Cow<'a, str>, String)> {
let left = &body[i];
let right = &body[i + 1];
if left.style == TokenStyle::Marker || right.style == TokenStyle::Marker {
return None;
}
let left_text = left.text.as_ref().expect("Token text is required");
let right_text = right.text.as_ref().expect("Token text is required");
let merged = merge_token_text(info, left, right);
Some((left_text.clone(), right_text.clone(), merged))
}
/// Tracks frequency counts for individual tokens and adjacent pairs
/// during the iterative merge phase.
struct MergeState<'a> {
/// Token text -> occurrence count in the body.
token_counts: HashMap<Cow<'a, str>, usize>,
/// (left text, right text) -> (merged text, occurrence count).
/// The merged text is used for cost calculations via `define_savings`.
pair_counts: HashMap<(Cow<'a, str>, Cow<'a, str>), (String, usize)>,
}
impl<'a> MergeState<'a> {
/// Builds initial counts from the body.
fn new(info: &impl TokenInfo, body: &Tokens<'a>) -> Self {
let mut token_counts: HashMap<Cow<'a, str>, usize> = HashMap::new();
let mut pair_counts: HashMap<(Cow<'a, str>, Cow<'a, str>), (String, usize)> =
HashMap::new();
for token in body.iter() {
if token.style == TokenStyle::Marker {
continue;
}
if let Some(text) = token.text.as_ref() {
*token_counts.entry(text.clone()).or_default() += 1;
}
}
for i in 0..body.len().saturating_sub(1) {
if let Some((left, right, merged)) = pair_at(info, body, i) {
pair_counts
.entry((left, right))
.and_modify(|v| v.1 += 1)
.or_insert((merged, 1));
}
}
Self {
token_counts,
pair_counts,
}
}
/// Finds the single most beneficial merge, or `None` if no
/// profitable merge remains.
/// Returns the left and right tokens which should be merged.
///
/// A merge is profitable when after > before, i.e. merging
/// lets us save more text than could be done with both tokens
/// individually.
fn find_best_merge(&self, replace_len: usize) -> Option<(Cow<'a, str>, Cow<'a, str>)> {
// This is used to keep a consistent sort order
// when we get the same benefit.
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct MaxKey<'a>(i32, Cow<'a, str>, Cow<'a, str>);
self.pair_counts
.iter()
.filter(|(_, (_, count))| *count > 0)
.filter_map(|((left, right), (pair_merged, pair_count))| {
let left_count = *self.token_counts.get(left)?;
let right_count = *self.token_counts.get(right)?;
if left_count == 0 || right_count == 0 {
return None;
}
// We have a min of 0 because if we don't save anything from making
// this token a define, we wouldn't do so in the first place, i.e.,
// zero savings.
let before = define_savings(left, left_count, replace_len).max(0)
+ define_savings(right, right_count, replace_len).max(0);
let after = define_savings(pair_merged, *pair_count, replace_len).max(0)
// left and right no longer get to claim the savings for this pair.
+ define_savings(left, left_count - *pair_count, replace_len).max(0)
+ define_savings(right, right_count - *pair_count, replace_len).max(0);
let benefit = after - before;
// This makes us stop iterating once we can't compress anymore.
if benefit > 0 {
Some((left.clone(), right.clone(), benefit))
} else {
None
}
})
.max_by_key(|(left, right, b)| MaxKey(*b, left.clone(), right.clone()))
.map(|(a, b, _)| (a, b))
}
}
/// Iteratively merges the most beneficial adjacent pair until no
/// profitable merges remain.
fn merge_pairs(info: &impl TokenInfo, body: &mut Tokens<'_>, replace_len: usize) {
loop {
// It's somewhat expensive to recompute the MergeState every iteration,
// but updating it correctly is very complex.
let state = MergeState::new(info, body);
let Some((merge_left, merge_right)) = state.find_best_merge(replace_len) else {
break;
};
info.merge_tokens(
body,
Some(&|left, right| {
left.style != TokenStyle::Marker
&& right.style != TokenStyle::Marker
&& left.text.as_ref() == Some(&merge_left)
&& right.text.as_ref() == Some(&merge_right)
}),
);
}
}
/// Assigns defines for tokens where the savings are positive, rewrites
/// `body` to use the short names, and prepends define lines to
/// `header`.
fn assign_defines<'a>(
info: &impl TokenInfo,
header: &mut Tokens<'a>,
body: &mut Tokens<'a>,
used: &HashSet<Cow<'a, str>>,
) {
// Count occurrences of each distinct token text.
let mut counts = HashMap::<String, usize>::new();
for token in body.iter() {
if token.style == TokenStyle::Marker {
continue;
}
*counts
.entry(
token
.text
.as_ref()
.expect("Token text is required")
.to_string(),
)
.or_default() += 1;
}
// Assign short names.
let mut name_num: usize = 0;
// Token -> define name.
let mut defines: Vec<(String, String)> = Vec::new();
// This is in a loop, rather than a single collect phase, because the replacements
// which are profitable can depend on whether they're in between identifiers.
// The neighbors of each token change as we do more replacements.
// Therefore, we must recompute at each step to determine profitability.
loop {
let define_name = next_name(&mut name_num, used);
// This is used to keep a consistent sort order
// when we get the same savings.
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct SortKey<'a>(i32, &'a str);
// Collect tokens worth defining, sorted by descending savings so
// that the most valuable tokens get the shortest names first.
let Some(to_replace) = counts
.iter()
.flat_map(|(token, &count)| {
// Swapping to a define can introduce spaces on either
// side which weren't previously there.
// We must factor this into our cost calculation.
//
// It may be possible to factor this into merge_pairs as well,
// although it's likely very complex.
let num_spaces: i32 = body
.array_windows::<3>()
.filter(|[_, middle, _]| middle.text.as_deref() == Some(token))
.map(|[left, middle, right]| {
// Use a dummy token that looks like an identifier (what the define replacement will be).
let left_space = info.needs_space_between(left, &Token::new("a".into()))
&& !info.needs_space_between(left, middle);
let right_space = info.needs_space_between(&Token::new("a".into()), right)
&& !info.needs_space_between(middle, right);
left_space as i32 + right_space as i32
})
.sum();
let s = define_savings(token, count, define_name.len()) - num_spaces;
if s <= 0 {
// This token is not worth compressing.
return None;
}
Some((token, s))
})
.max_by_key(|(text, savings)| SortKey(*savings, text))
.map(|(text, _)| text.clone())
else {
break;
};
counts.remove(&to_replace);
defines.push((to_replace.clone(), define_name.to_string()));
for token in body.iter_mut() {
if token.style == TokenStyle::Marker {
continue;
}
let text = token.text.as_ref().expect("Token text is required");
if text.as_ref() == to_replace {
token.text = Some(define_name.to_string().into());
}
}
}
// Prepend define lines to header (reversed so the first define
// ends up at the top).
//
// If the original token contains newlines, we must insert backslash
// characters so it spans multiple lines.
for (original, name) in defines.iter().rev() {
// The newline is still inserted literally, just with a backslash behind it.
let escaped = original.replace('\n', "\\\n");
// TODO: Replace tokens inside this define with other defines.
header.push(Token::new(format!("#define {} {}\n", name, escaped).into()));
}
}
/// Compresses the given body by introducing define macros into the
/// header.
/// This MUST be run before merging any tokens.
pub fn compress_with_defines<'a>(
info: &impl TokenInfo,
header: &mut Tokens<'a>,
body: &mut Tokens<'a>,
) {
if body.len() <= 1 {
return;
}
// Record used names before merging destroys individual tokens.
// This lets us avoid conflicts when assigning names to defines.
let used: HashSet<Cow<'a, str>> = iter::chain(header.iter(), body.iter())
.flat_map(|token| token.text.clone())
.collect();
// Use the minimum possible next identifier size as a good estimate
// to use for calculating define cost for merging.
// This doesn't take into account the size increasing midway through,
// once we reach the next length, but doing is very complex,
// and merge_pairs does do it, so we won't create defines that
// make the output longer.
let replace_len = next_name(&mut 0, &used).len();
// Merge adjacent tokens if doing so allows saving more space
// overall.
// For example, if a is always followed by b, then we'd want to merge
// a and b, since it always allows saving more space (1 define instead of 2).
// However, if a is only followed by c one time, then it doesn't make sense
// to merge them.
//
// The main complexity is with cases in between these, as it may make sense to merge
// both a+b and a+d.
merge_pairs(info, body, replace_len);
// Now that tokens are maximized, compress the ones with the biggest savings by
// turning them into defines.
assign_defines(info, header, body, &used);
}