1 //! Code to compute example inputs given a backtrace.
2 
3 use ascii_canvas::AsciiView;
4 use grammar::repr::*;
5 use message::builder::InlineBuilder;
6 use message::Content;
7 use std::fmt::{Debug, Error, Formatter};
8 use style::Style;
9 use tls::Tls;
10 
11 #[cfg(test)]
12 mod test;
13 
14 /// An "example" input and the way it was derived. This can be
15 /// serialized into useful text. For example, it might represent
16 /// something like this:
17 ///
18 /// ```
19 ///          Looking at
20 ///              |
21 ///              v
22 /// Ty "->" Ty "->" Ty
23 /// |        |       |
24 /// +-Ty-----+       |
25 /// |                |
26 /// +-Ty-------------+
27 /// ```
28 ///
29 /// The top-line is the `symbols` vector. The groupings below are
30 /// stored in the `reductions` vector, in order from smallest to
31 /// largest (they are always properly nested). The `cursor` field
32 /// indicates the current lookahead token.
33 ///
34 /// The `symbols` vector is actually `Option<Symbol>` to account
35 /// for empty reductions:
36 ///
37 /// ```
38 /// A       B
39 /// | |   | |
40 /// | +-Y-+ |
41 /// +-Z-----+
42 /// ```
43 ///
44 /// The "empty space" between A and B would be represented as `None`.
45 #[derive(Clone, Debug)]
46 pub struct Example {
47     pub symbols: Vec<ExampleSymbol>,
48     pub cursor: usize,
49     pub reductions: Vec<Reduction>,
50 }
51 
52 #[derive(Clone, Debug, PartialEq, Eq)]
53 pub enum ExampleSymbol {
54     Symbol(Symbol),
55     Epsilon,
56 }
57 
58 #[derive(Copy, Clone, Default)]
59 pub struct ExampleStyles {
60     pub before_cursor: Style,
61     pub on_cursor: Style,
62     pub after_cursor: Style,
63 }
64 
65 #[derive(Clone, Debug)]
66 pub struct Reduction {
67     pub start: usize,
68     pub end: usize,
69     pub nonterminal: NonterminalString,
70 }
71 
72 impl Example {
73     /// Length of each symbol. Each will need *at least* that amount
74     /// of space. :) Measure in characters, under the assumption of a
75     /// mono-spaced font. Also add a final `0` marker which will serve
76     /// as the end position.
lengths(&self) -> Vec<usize>77     fn lengths(&self) -> Vec<usize> {
78         self.symbols
79             .iter()
80             .map(|s| match *s {
81                 ExampleSymbol::Symbol(ref s) => format!("{}", s).chars().count(),
82                 ExampleSymbol::Epsilon => 1, // display as " "
83             })
84             .chain(Some(0))
85             .collect()
86     }
87 
88     /// Extract a prefix of the list of symbols from this `Example`
89     /// and make a styled list of them, like:
90     ///
91     ///    Ty "->" Ty -> "Ty"
to_symbol_list(&self, length: usize, styles: ExampleStyles) -> Box<dyn Content>92     pub fn to_symbol_list(&self, length: usize, styles: ExampleStyles) -> Box<dyn Content> {
93         let mut builder = InlineBuilder::new().begin_spaced();
94 
95         for (index, symbol) in self.symbols[..length].iter().enumerate() {
96             let style = if index < self.cursor {
97                 styles.before_cursor
98             } else if index > self.cursor {
99                 styles.after_cursor
100             } else {
101                 match *symbol {
102                     ExampleSymbol::Symbol(Symbol::Terminal(_)) => styles.on_cursor,
103                     ExampleSymbol::Symbol(Symbol::Nonterminal(_)) => styles.after_cursor,
104                     ExampleSymbol::Epsilon => styles.after_cursor,
105                 }
106             };
107 
108             if let ExampleSymbol::Symbol(ref s) = symbol {
109                 builder = builder.push(s.clone()).styled(style);
110             }
111         }
112 
113         builder.end().indented().end()
114     }
115 
116     /// Render the example into a styled diagram suitable for
117     /// embedding in an error message.
into_picture(self, styles: ExampleStyles) -> Box<dyn Content>118     pub fn into_picture(self, styles: ExampleStyles) -> Box<dyn Content> {
119         let lengths = self.lengths();
120         let positions = self.positions(&lengths);
121         InlineBuilder::new()
122             .push(Box::new(ExamplePicture {
123                 example: self,
124                 positions,
125                 styles,
126             }))
127             .indented()
128             .end()
129     }
130 
starting_positions(&self, lengths: &[usize]) -> Vec<usize>131     fn starting_positions(&self, lengths: &[usize]) -> Vec<usize> {
132         lengths
133             .iter()
134             .scan(0, |counter, &len| {
135                 let start = *counter;
136 
137                 // Leave space for "NT " (if "NT" is the name
138                 // of the nonterminal).
139                 *counter = start + len + 1;
140 
141                 Some(start)
142             })
143             .collect()
144     }
145 
146     /// Start index where each symbol in the example should appear,
147     /// measured in characters. These are spaced to leave enough room
148     /// for the reductions below.
positions(&self, lengths: &[usize]) -> Vec<usize>149     fn positions(&self, lengths: &[usize]) -> Vec<usize> {
150         // Initially, position each symbol with one space in between,
151         // like:
152         //
153         //     X Y Z
154         let mut positions = self.starting_positions(lengths);
155 
156         // Adjust spacing to account for the nonterminal labels
157         // we will have to add. It will display
158         // like this:
159         //
160         //    A1 B2 C3 D4 E5 F6
161         //    |         |
162         //    +-Label---+
163         //
164         // But if the label is long we may have to adjust the spacing
165         // of the covered items (here, we changed them to two spaces,
166         // except the first gap, which got 3 spaces):
167         //
168         //    A1   B2  C3  D4 E5 F6
169         //    |             |
170         //    +-LongLabel22-+
171         for &Reduction {
172             start,
173             end,
174             ref nonterminal,
175         } in &self.reductions
176         {
177             let nt_len = format!("{}", nonterminal).chars().count();
178 
179             // Number of symbols we are reducing. This should always
180             // be non-zero because even in the case of a \epsilon
181             // rule, we ought to be have a `None` entry in the symbol array.
182             let num_syms = end - start;
183             assert!(num_syms > 0);
184 
185             // Let's use the expansion from above as our running example.
186             // We start out with positions like this:
187             //
188             //    A1 B2 C3 D4 E5 F6
189             //    |             |
190             //    +-LongLabel22-+
191             //
192             // But we want LongLabel to end at D4. No good.
193 
194             // Start of first symbol to be reduced. Here, 0.
195             //
196             // A1 B2 C3 D4
197             // ^ here
198             let start_position = positions[start];
199 
200             // End of last symbol to be reduced. Here, 11.
201             //
202             // A1 B2 C3 D4 E5
203             //             ^ positions[end]
204             //            ^ here -- positions[end] - 1
205             let end_position = positions[end] - 1;
206 
207             // We need space to draw `+-Label-+` between
208             // start_position and end_position.
209             let required_len = nt_len + 4; // here, 15
210             let actual_len = end_position - start_position; // here, 10
211             if required_len < actual_len {
212                 continue; // Got enough space, all set.
213             }
214 
215             // Have to add `difference` characters altogether.
216             let difference = required_len - actual_len; // here, 4
217 
218             // Increment over everything that is not part of this nonterminal.
219             // In the example above, that is E5 and F6.
220             shift(&mut positions[end..], difference);
221 
222             if num_syms > 1 {
223                 // If there is just one symbol being reduced here,
224                 // then we have shifted over the things that follow
225                 // it, and we are done. This would be a case like:
226                 //
227                 //     X         Y Z
228                 //     |       |
229                 //     +-Label-+
230                 //
231                 // (which maybe ought to be rendered slightly
232                 // differently).
233                 //
234                 // But if there are multiple symbols, we're not quite
235                 // done, because there would be an unsightly gap:
236                 //
237                 //       (gaps)
238                 //      |  |  |
239                 //      v  v  v
240                 //    A1 B2 C3 D4     E5 F6
241                 //    |             |
242                 //    +-LongLabel22-+
243                 //
244                 // we'd like to make things line up, so we have to
245                 // distribute that extra space internally by
246                 // increasing the "gaps" (marked above) as evenly as
247                 // possible (basically, full justification).
248                 //
249                 // We do this by dividing up the spaces evenly and
250                 // then taking the remainder `N` and distributing 1
251                 // extra to the first N.
252                 let num_gaps = num_syms - 1; // number of gaps we can adjust. Here, 3.
253                 let amount = difference / num_gaps; // what to add to each gap. Here, 1.
254                 let extra = difference % num_gaps; // the remainder. Here, 1.
255 
256                 // For the first `extra` symbols, give them amount + 1
257                 // extra space. After that, just amount. (O(n^2). Sue me.)
258                 for i in 0..extra {
259                     shift(&mut positions[start + 1 + i..end], amount + 1);
260                 }
261                 for i in extra..num_gaps {
262                     shift(&mut positions[start + 1 + i..end], amount);
263                 }
264             }
265         }
266 
267         positions
268     }
269 
270     #[cfg(test)]
paint_unstyled(&self) -> Vec<::ascii_canvas::Row>271     pub fn paint_unstyled(&self) -> Vec<::ascii_canvas::Row> {
272         let this = self.clone();
273         let content = this.into_picture(ExampleStyles::default());
274         let min_width = content.min_width();
275         let canvas = content.emit_to_canvas(min_width);
276         canvas.to_strings()
277     }
278 
paint_on(&self, styles: &ExampleStyles, positions: &[usize], view: &mut dyn AsciiView)279     fn paint_on(&self, styles: &ExampleStyles, positions: &[usize], view: &mut dyn AsciiView) {
280         // Draw the brackets for each reduction:
281         for (index, reduction) in self.reductions.iter().enumerate() {
282             let start_column = positions[reduction.start];
283             let end_column = positions[reduction.end] - 1;
284             let row = 1 + index;
285             view.draw_vertical_line(0..row + 1, start_column);
286             view.draw_vertical_line(0..row + 1, end_column - 1);
287             view.draw_horizontal_line(row, start_column..end_column);
288         }
289 
290         // Write the labels for each reduction. Do this after the
291         // brackets so that ascii canvas can convert `|` to `+`
292         // without interfering with the text (in case of weird overlap).
293         let session = Tls::session();
294         for (index, reduction) in self.reductions.iter().enumerate() {
295             let column = positions[reduction.start] + 2;
296             let row = 1 + index;
297             view.write_chars(
298                 row,
299                 column,
300                 reduction.nonterminal.to_string().chars(),
301                 session.nonterminal_symbol,
302             );
303         }
304 
305         // Write the labels on top:
306         //    A1   B2  C3  D4 E5 F6
307         self.paint_symbols_on(&self.symbols, &positions, styles, view);
308     }
309 
paint_symbols_on( &self, symbols: &[ExampleSymbol], positions: &[usize], styles: &ExampleStyles, view: &mut dyn AsciiView, )310     fn paint_symbols_on(
311         &self,
312         symbols: &[ExampleSymbol],
313         positions: &[usize],
314         styles: &ExampleStyles,
315         view: &mut dyn AsciiView,
316     ) {
317         let session = Tls::session();
318         for (index, ex_symbol) in symbols.iter().enumerate() {
319             let style = if index < self.cursor {
320                 styles.before_cursor
321             } else if index == self.cursor {
322                 // Only display actual terminals in the "on-cursor"
323                 // font, because it might be misleading to show a
324                 // nonterminal that way. Really it'd be nice to expand
325                 // so that the cursor is always a terminal.
326                 match *ex_symbol {
327                     ExampleSymbol::Symbol(Symbol::Terminal(_)) => styles.on_cursor,
328                     _ => styles.after_cursor,
329                 }
330             } else {
331                 styles.after_cursor
332             };
333 
334             let column = positions[index];
335             match *ex_symbol {
336                 ExampleSymbol::Symbol(Symbol::Terminal(ref term)) => {
337                     view.write_chars(
338                         0,
339                         column,
340                         term.to_string().chars(),
341                         style.with(session.terminal_symbol),
342                     );
343                 }
344                 ExampleSymbol::Symbol(Symbol::Nonterminal(ref nt)) => {
345                     view.write_chars(
346                         0,
347                         column,
348                         nt.to_string().chars(),
349                         style.with(session.nonterminal_symbol),
350                     );
351                 }
352                 ExampleSymbol::Epsilon => {}
353             }
354         }
355     }
356 }
357 
358 struct ExamplePicture {
359     example: Example,
360     positions: Vec<usize>,
361     styles: ExampleStyles,
362 }
363 
364 impl Content for ExamplePicture {
min_width(&self) -> usize365     fn min_width(&self) -> usize {
366         *self.positions.last().unwrap()
367     }
368 
emit(&self, view: &mut dyn AsciiView)369     fn emit(&self, view: &mut dyn AsciiView) {
370         self.example.paint_on(&self.styles, &self.positions, view);
371     }
372 
into_wrap_items(self: Box<Self>, wrap_items: &mut Vec<Box<dyn Content>>)373     fn into_wrap_items(self: Box<Self>, wrap_items: &mut Vec<Box<dyn Content>>) {
374         wrap_items.push(self);
375     }
376 }
377 
378 impl Debug for ExamplePicture {
fmt(&self, fmt: &mut Formatter) -> Result<(), Error>379     fn fmt(&self, fmt: &mut Formatter) -> Result<(), Error> {
380         Debug::fmt(&self.example, fmt)
381     }
382 }
383 
shift(positions: &mut [usize], amount: usize)384 fn shift(positions: &mut [usize], amount: usize) {
385     for position in positions {
386         *position += amount;
387     }
388 }
389 
390 impl ExampleStyles {
ambig() -> Self391     pub fn ambig() -> Self {
392         let session = Tls::session();
393         ExampleStyles {
394             before_cursor: session.ambig_symbols,
395             on_cursor: session.ambig_symbols,
396             after_cursor: session.ambig_symbols,
397         }
398     }
399 
new() -> Self400     pub fn new() -> Self {
401         let session = Tls::session();
402         ExampleStyles {
403             before_cursor: session.observed_symbols,
404             on_cursor: session.cursor_symbol,
405             after_cursor: session.unobserved_symbols,
406         }
407     }
408 }
409