use crate::gc::GcRef;
#[derive(Clone, Debug)]
pub struct ParseFail {
pub input_span: (usize, usize),
pub expected: String,
pub parser_span: Option<(u32, u32)>,
pub partial: Option<GcRef>,
}
impl ParseFail {
pub fn here(offset: usize, expected: impl Into<String>) -> Self {
ParseFail {
input_span: (offset, offset),
expected: expected.into(),
parser_span: None,
partial: None,
}
}
pub fn at(offset: usize, len: usize, expected: impl Into<String>) -> Self {
ParseFail {
input_span: (offset, offset + len),
expected: expected.into(),
parser_span: None,
partial: None,
}
}
#[must_use]
pub fn with_parser_span(mut self, span: Option<(u32, u32)>) -> Self {
self.parser_span = span;
self
}
#[must_use]
pub fn with_partial(mut self, partial: Option<GcRef>) -> Self {
self.partial = partial;
self
}
}
#[derive(Debug, Default)]
pub struct ParseDetail {
pub fail: Option<ParseFail>,
pub actual_preview: String,
}
impl ParseDetail {
pub fn new() -> Self {
ParseDetail::default()
}
pub fn clear(&mut self) {
self.fail = None;
self.actual_preview.clear();
}
pub fn is_set(&self) -> bool {
self.fail.is_some()
}
pub fn consider(&mut self, fail: ParseFail, input: &[u8]) {
let wins = match &self.fail {
None => true,
Some(existing) => fail.input_span.0 > existing.input_span.0,
};
if wins {
self.actual_preview = preview_around(input, fail.input_span.0);
self.fail = Some(fail);
}
}
}
const PREVIEW_RADIUS: usize = 24;
fn preview_around(input: &[u8], offset: usize) -> String {
let end = (offset + PREVIEW_RADIUS).min(input.len());
let start = offset.saturating_sub(PREVIEW_RADIUS).min(end);
let slice = &input[start..end];
let lossy = String::from_utf8_lossy(slice);
let mut out = String::with_capacity(lossy.len());
for ch in lossy.chars() {
if ch == '\n' || ch == '\r' {
out.push('⏎');
} else {
out.push(ch);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_failure_offset_past_the_buffer_previews_rather_than_panicking() {
let mut d = ParseDetail::new();
d.consider(ParseFail::here(10_000, "int"), b"short");
assert!(d.is_set());
assert_eq!(
d.actual_preview, "",
"there is nothing within 24 bytes of an offset past the end, and nothing is a preview"
);
}
#[test]
fn first_failure_sets_detail() {
let mut d = ParseDetail::new();
d.consider(ParseFail::here(5, "int"), b"abc123");
assert!(d.is_set());
assert_eq!(d.fail.as_ref().unwrap().input_span, (5, 5));
assert_eq!(d.fail.as_ref().unwrap().expected, "int");
}
#[test]
fn deeper_failure_wins() {
let mut d = ParseDetail::new();
d.consider(ParseFail::here(3, "outer"), b"abcdef");
d.consider(ParseFail::here(10, "inner"), b"abcdef");
assert_eq!(d.fail.as_ref().unwrap().expected, "inner");
assert_eq!(d.fail.as_ref().unwrap().input_span.0, 10);
}
#[test]
fn shallower_failure_loses() {
let mut d = ParseDetail::new();
d.consider(ParseFail::here(20, "deep"), b"abcdef");
d.consider(ParseFail::here(5, "shallow"), b"abcdef");
assert_eq!(d.fail.as_ref().unwrap().expected, "deep");
}
#[test]
fn tie_keeps_first() {
let mut d = ParseDetail::new();
d.consider(ParseFail::here(8, "first"), b"abcdef");
d.consider(ParseFail::here(8, "second"), b"abcdef");
assert_eq!(d.fail.as_ref().unwrap().expected, "first");
}
#[test]
fn clear_resets() {
let mut d = ParseDetail::new();
d.consider(ParseFail::here(3, "int"), b"abc");
d.clear();
assert!(!d.is_set());
assert!(d.actual_preview.is_empty());
}
#[test]
fn preview_is_single_line_and_bounded() {
let input = b"aaaa\nbbbb\ncccc\ndddd\neeee";
let preview = preview_around(input, 12);
assert!(!preview.contains('\n'));
assert!(!preview.contains('\r'));
assert!(preview.contains('⏎'));
assert!(preview.chars().count() <= 2 * PREVIEW_RADIUS + 4);
}
#[test]
fn preview_at_buffer_start() {
let preview = preview_around(b"hello world", 0);
assert!(preview.starts_with("hello"));
}
#[test]
fn preview_at_buffer_end() {
let preview = preview_around(b"hello world", 11);
assert!(preview.ends_with("world"));
}
}