use alloc::{string::String, vec::Vec};
use source_lang::{SourceFile, SourceId, SourceMap, Span};
use crate::{Entry, Feed, Input, SessionError, Status};
#[derive(Clone, Debug)]
pub struct Session {
sources: SourceMap,
ids: Vec<SourceId>,
pending: String,
name: String,
next_base: u32,
limit: usize,
}
impl Default for Session {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl Session {
pub const DEFAULT_LIMIT: usize = 1 << 20;
#[must_use]
pub fn new() -> Self {
Self::with_limit(Self::DEFAULT_LIMIT)
}
#[must_use]
pub fn with_limit(limit: usize) -> Self {
Self {
sources: SourceMap::new(),
ids: Vec::new(),
pending: String::new(),
name: String::new(),
next_base: 0,
limit,
}
}
pub fn feed<T, F>(&mut self, line: &str, pipeline: F) -> Result<Feed<T>, SessionError>
where
F: FnOnce(Input<'_>) -> Status<T>,
{
let line = strip_terminator(line);
if self.pending.is_empty() && line.trim().is_empty() {
return Ok(Feed::Empty);
}
self.admit(line)?;
self.pending.push_str(line);
self.pending.push('\n');
let number = self.next_number();
match pipeline(Input::new(&self.pending, self.next_base, number)) {
Status::Incomplete => Ok(Feed::Incomplete),
Status::Complete(value) => {
let entry = self.commit(number)?;
Ok(Feed::Complete { entry, value })
}
}
}
pub fn cancel(&mut self) -> bool {
let had = !self.pending.is_empty();
self.pending.clear();
had
}
#[inline]
#[must_use]
pub fn is_pending(&self) -> bool {
!self.pending.is_empty()
}
#[inline]
#[must_use]
pub fn pending(&self) -> &str {
&self.pending
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.ids.len()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.ids.is_empty()
}
#[must_use]
pub fn entry(&self, number: u32) -> Option<&SourceFile> {
let index = usize::try_from(number).ok()?.checked_sub(1)?;
self.sources.source(*self.ids.get(index)?)
}
#[inline]
#[must_use]
pub fn sources(&self) -> &SourceMap {
&self.sources
}
#[inline]
#[must_use]
pub fn limit(&self) -> usize {
self.limit
}
fn admit(&mut self, line: &str) -> Result<(), SessionError> {
let len = self
.pending
.len()
.saturating_add(line.len())
.saturating_add(1);
let error = if len > self.limit {
SessionError::TooLong {
len,
limit: self.limit,
}
} else {
let available = u64::from(u32::MAX - self.next_base);
let needed = len as u64;
if needed <= available {
return Ok(());
}
SessionError::SpaceExhausted { needed, available }
};
self.pending.clear();
Err(error)
}
#[inline]
fn next_number(&self) -> u32 {
u32::try_from(self.ids.len()).map_or(u32::MAX, |n| n.saturating_add(1))
}
fn commit(&mut self, number: u32) -> Result<Entry, SessionError> {
self.name.clear();
self.name.push_str("<repl:");
push_decimal(&mut self.name, number);
self.name.push('>');
let needed = self.pending.len() as u64;
let added = self.sources.add(self.name.as_str(), self.pending.as_str());
self.pending.clear();
let id = added.map_err(|_| SessionError::SpaceExhausted {
needed,
available: u64::from(u32::MAX - self.next_base),
})?;
let len = needed as u32;
let span = Span::new(self.next_base, self.next_base + len);
self.next_base += len;
self.ids.push(id);
Ok(Entry::new(number, id, span))
}
}
#[inline]
fn strip_terminator(line: &str) -> &str {
match line.strip_suffix('\n') {
Some(rest) => rest.strip_suffix('\r').unwrap_or(rest),
None => line,
}
}
fn push_decimal(out: &mut String, mut n: u32) {
let mut digits = [0_u8; 10];
let mut start = digits.len();
loop {
start -= 1;
digits[start] = b'0' + (n % 10) as u8;
n /= 10;
if n == 0 {
break;
}
}
for &digit in &digits[start..] {
out.push(char::from(digit));
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic, unused_results)]
mod tests {
use alloc::{borrow::ToOwned, string::ToString};
use super::*;
fn complete(input: Input<'_>) -> Status<String> {
Status::Complete(input.text().to_owned())
}
fn incomplete(_: Input<'_>) -> Status<String> {
Status::Incomplete
}
#[test]
fn test_feed_blank_line_with_nothing_pending_is_empty() {
let mut session = Session::new();
for blank in ["", " ", "\n", "\r\n", "\t \n"] {
let feed = session.feed(blank, |_| -> Status<()> { panic!("pipeline called") });
assert_eq!(feed, Ok(Feed::Empty));
}
assert!(session.is_empty());
}
#[test]
fn test_feed_complete_commits_entry() {
let mut session = Session::new();
let feed = session.feed("x = 1", complete).unwrap();
let Feed::Complete { entry, value } = feed else {
panic!("expected complete, got {feed:?}");
};
assert_eq!(value, "x = 1\n");
assert_eq!(entry.number(), 1);
assert_eq!(entry.span(), Span::new(0, 6));
assert_eq!(session.len(), 1);
assert!(!session.is_pending());
let source = session.sources().source(entry.id()).unwrap();
assert_eq!(source.name(), "<repl:1>");
assert_eq!(source.text(), "x = 1\n");
assert_eq!(source.span(), entry.span());
}
#[test]
fn test_feed_incomplete_accumulates_lines() {
let mut session = Session::new();
assert_eq!(session.feed("a", incomplete), Ok(Feed::Incomplete));
assert_eq!(session.feed("", incomplete), Ok(Feed::Incomplete));
assert_eq!(session.feed("b", incomplete), Ok(Feed::Incomplete));
assert_eq!(session.pending(), "a\n\nb\n");
assert!(session.is_empty());
}
#[test]
fn test_feed_blank_line_inside_entry_reaches_pipeline() {
let mut session = Session::new();
session.feed("def f():", incomplete).unwrap();
let mut seen = String::new();
session
.feed("", |input| {
seen.push_str(input.text());
Status::Complete(())
})
.unwrap();
assert_eq!(seen, "def f():\n\n");
}
#[test]
fn test_feed_strips_one_terminator() {
let mut session = Session::new();
let texts: Vec<String> = ["a\n", "b\r\n", "c", "d\n\n", "e\r"]
.iter()
.map(|line| match session.feed(line, complete).unwrap() {
Feed::Complete { value, .. } => value,
other => panic!("{other:?}"),
})
.collect();
assert_eq!(texts, ["a\n", "b\n", "c\n", "d\n\n", "e\r\n"]);
}
#[test]
fn test_feed_bases_are_contiguous() {
let mut session = Session::new();
let mut bases = Vec::new();
for line in ["one", "two", "three"] {
session
.feed(line, |input| {
bases.push(input.base().to_u32());
Status::Complete(())
})
.unwrap();
}
assert_eq!(bases, [0, 4, 8]);
assert_eq!(session.entry(3).unwrap().span(), Span::new(8, 14));
}
#[test]
fn test_feed_number_stable_while_incomplete() {
let mut session = Session::new();
let mut numbers = Vec::new();
for (line, done) in [("a", false), ("b", true), ("c", true)] {
session
.feed(line, |input| {
numbers.push(input.number());
if done {
Status::Complete(())
} else {
Status::Incomplete
}
})
.unwrap();
}
assert_eq!(numbers, [1, 1, 2]);
}
#[test]
fn test_feed_too_long_discards_pending_and_skips_pipeline() {
let mut session = Session::with_limit(5);
session.feed("ab", incomplete).unwrap(); let err = session
.feed("cd", |_| -> Status<()> { panic!("pipeline called") })
.unwrap_err();
assert_eq!(err, SessionError::TooLong { len: 6, limit: 5 });
assert!(!session.is_pending());
assert!(matches!(
session.feed("ok", complete),
Ok(Feed::Complete { .. })
));
}
#[test]
fn test_feed_exactly_at_limit_is_accepted() {
let mut session = Session::with_limit(4);
assert!(matches!(
session.feed("abc", complete),
Ok(Feed::Complete { .. })
));
}
#[test]
fn test_feed_zero_limit_refuses_everything_but_blanks() {
let mut session = Session::with_limit(0);
assert_eq!(session.feed("", complete), Ok(Feed::Empty));
assert!(matches!(
session.feed("x", complete),
Err(SessionError::TooLong { .. })
));
}
#[test]
fn test_feed_space_exhausted_when_map_full() {
let mut session = Session::with_limit(usize::MAX);
session.next_base = u32::MAX - 3; let err = session.feed("abcd", complete).unwrap_err();
assert_eq!(
err,
SessionError::SpaceExhausted {
needed: 5,
available: 3
}
);
assert!(!session.is_pending());
}
#[test]
fn test_cancel_keeps_entry_number() {
let mut session = Session::new();
session.feed("x", incomplete).unwrap();
assert!(session.cancel());
let mut number = 0;
session
.feed("y", |input| {
number = input.number();
Status::Complete(())
})
.unwrap();
assert_eq!(number, 1);
}
#[test]
fn test_entry_out_of_range_is_none() {
let mut session = Session::new();
session.feed("x", complete).unwrap();
assert!(session.entry(0).is_none());
assert!(session.entry(2).is_none());
assert!(session.entry(u32::MAX).is_none());
assert_eq!(session.entry(1).unwrap().text(), "x\n");
}
#[test]
fn test_push_decimal_formats_like_display() {
for n in [0, 7, 10, 99, 100, 12_345, u32::MAX] {
let mut out = String::new();
push_decimal(&mut out, n);
assert_eq!(out, n.to_string());
}
}
#[test]
fn test_session_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Session>();
assert_send_sync::<crate::Editor>();
}
}