pub struct Session { /* private fields */ }Expand description
The state of one REPL session: the entry being typed, and every entry accepted so far.
A REPL reads a line, decides whether it finishes a piece of code, and if
not keeps reading. Session owns that decision loop without owning the
language. Each call to feed appends a line to the
pending entry and runs the caller’s pipeline — lex, parse, evaluate,
whatever the language does — on all of it. The pipeline answers
Status::Complete with its result, or Status::Incomplete to ask for
another line.
A completed entry is committed to the session’s SourceMap as a source
named <repl:N>, laid out after the entries before it in one global
position space. The pipeline sees that position before the entry is
committed (as Input::base), so the spans it records are final the
moment they are made. A diagnostic produced by entry 7 can point at a
definition in entry 2, and a renderer given sources
shows both lines with the right names.
The session does no I/O. Pair it with any line source: an Editor
driven by a terminal, stdin read line by line, a script, a test.
§Cost
While an entry is incomplete the pipeline re-reads it in full for every
new line, so an entry of k lines is processed k times. Interactive
entries are short and this keeps the pipeline stateless. A host that
receives a large paste should feed it as one chunk: feed accepts text
containing newlines. with_limit caps entry size.
The pending buffer is reused across entries. Committing an entry makes one exact-size copy of its text and its name into the source map; otherwise feeding allocates only when the pending buffer grows past its previous size.
§Examples
A REPL loop over a fixed script, with a pipeline that waits for balanced parentheses and then reports how many lines the entry spanned:
use repl_lang::{Feed, Session, Status};
let mut session = Session::new();
let mut results = Vec::new();
for line in ["(define (square x)", " (* x x))", "", "(square 4)"] {
let feed = session.feed(line, |input| {
let depth: i32 = input.text().chars().map(|c| match c {
'(' => 1,
')' => -1,
_ => 0,
}).sum();
if depth > 0 { Status::Incomplete } else { Status::Complete(input.text().lines().count()) }
})?;
if let Feed::Complete { entry, value } = feed {
results.push((entry.number(), value));
}
}
assert_eq!(results, [(1, 2), (2, 1)]);
assert_eq!(session.len(), 2);
assert_eq!(session.entry(1).map(|e| e.text()), Some("(define (square x)\n (* x x))\n"));Implementations§
Source§impl Session
impl Session
Sourcepub const DEFAULT_LIMIT: usize
pub const DEFAULT_LIMIT: usize
The entry size limit of Session::new: 1 MiB.
Sourcepub fn new() -> Self
pub fn new() -> Self
Creates a session with no entries and the
DEFAULT_LIMIT on entry size.
§Examples
use repl_lang::Session;
let session = Session::new();
assert!(session.is_empty());
assert!(!session.is_pending());
assert_eq!(session.limit(), Session::DEFAULT_LIMIT);Sourcepub fn with_limit(limit: usize) -> Self
pub fn with_limit(limit: usize) -> Self
Creates a session whose entries may grow to at most limit bytes,
counting the '\n' that ends each line.
A line that would take the pending entry past the limit is refused with
SessionError::TooLong, and the entry is discarded. The limit bounds
memory held for one entry and the work the pipeline repeats on each
continuation line, so lower it when input is untrusted.
§Examples
use repl_lang::{Feed, Session, SessionError, Status};
let mut session = Session::with_limit(16);
let more = |_: repl_lang::Input<'_>| Status::<()>::Incomplete;
assert!(matches!(session.feed("0123456789", more)?, Feed::Incomplete)); // 11 bytes
let err = session.feed("abcdef", more).unwrap_err(); // would be 18
assert_eq!(err, SessionError::TooLong { len: 18, limit: 16 });
assert!(!session.is_pending());Sourcepub fn feed<T, F>(
&mut self,
line: &str,
pipeline: F,
) -> Result<Feed<T>, SessionError>
pub fn feed<T, F>( &mut self, line: &str, pipeline: F, ) -> Result<Feed<T>, SessionError>
Appends a line to the pending entry and runs pipeline on the whole
entry.
line is one line of input, with or without its line terminator; a
single trailing "\n" or "\r\n" is removed and a '\n' appended, so
lines from read_line and from str::lines behave the same. It may
also be several lines at once, as from a paste.
What happens next:
- If no entry is pending and
lineis blank, nothing is evaluated: the result isFeed::Emptyand the pipeline is not called. - Otherwise the line joins the pending entry and the pipeline is called
with an
Inputover all of it. - On
Status::Incompletethe entry stays pending and the result isFeed::Incomplete. Blank lines inside an entry are passed through, so a language can use one to end a block. - On
Status::Completethe entry is committed as<repl:N>and the result isFeed::Completewith the pipeline’s value. The pending buffer is cleared for the next entry.
§Errors
SessionError::TooLongif the entry would exceedlimit.SessionError::SpaceExhaustedif the session’s source map cannot hold the entry.
In both cases the pending entry is discarded and the pipeline is not called, so the session is ready for a fresh entry.
§Examples
Feeding lines straight from read_line, terminators included:
use repl_lang::{Feed, Session, Status};
let stdin_lines = ["let x =\r\n", " 42\n"];
let mut session = Session::new();
let mut last = None;
for raw in stdin_lines {
last = Some(session.feed(raw, |input| {
if input.text().trim_end().ends_with('=') {
Status::Incomplete
} else {
Status::Complete(input.text().to_owned())
}
})?);
}
let Some(Feed::Complete { value, .. }) = last else { return Err("incomplete".into()) };
assert_eq!(value, "let x =\n 42\n");The pipeline can capture the interpreter state it needs, mutably:
use repl_lang::{Feed, Session, Status};
let mut total = 0_i64;
let mut session = Session::new();
for line in ["5", "10", "-3"] {
session.feed(line, |input| match input.text().trim().parse::<i64>() {
Ok(n) => { total += n; Status::Complete(Ok(total)) }
Err(e) => Status::Complete(Err(e)),
})?;
}
assert_eq!(total, 12);Sourcepub fn cancel(&mut self) -> bool
pub fn cancel(&mut self) -> bool
Discards the pending entry, returning true if there was one.
This is the session half of an interrupt (Ctrl-C): drop the
half-typed entry and go back to the primary prompt. Committed entries
are unaffected and the next entry number is unchanged.
§Examples
use repl_lang::{Session, Status};
let mut session = Session::new();
session.feed("if x {", |_| Status::<()>::Incomplete)?;
assert!(session.is_pending());
assert!(session.cancel());
assert!(!session.is_pending());
assert!(!session.cancel());Sourcepub fn is_pending(&self) -> bool
pub fn is_pending(&self) -> bool
Returns true while an entry is incomplete — when the next line
continues it rather than starting a new one.
Hosts use it to choose between the primary prompt and the continuation prompt.
§Examples
use repl_lang::{Session, Status};
let mut session = Session::new();
let prompt = |s: &Session| if s.is_pending() { "... " } else { ">>> " };
assert_eq!(prompt(&session), ">>> ");
session.feed("[1,", |_| Status::<()>::Incomplete)?;
assert_eq!(prompt(&session), "... ");Sourcepub fn pending(&self) -> &str
pub fn pending(&self) -> &str
The text of the pending entry: every line fed since the last complete
entry, each ending in '\n'. Empty when nothing is pending.
§Examples
use repl_lang::{Session, Status};
let mut session = Session::new();
session.feed("a", |_| Status::<()>::Incomplete)?;
session.feed("b", |_| Status::<()>::Incomplete)?;
assert_eq!(session.pending(), "a\nb\n");Sourcepub fn len(&self) -> usize
pub fn len(&self) -> usize
The number of committed entries.
§Examples
use repl_lang::{Session, Status};
let mut session = Session::new();
session.feed("one", |_| Status::Complete(()))?;
session.feed("", |_| Status::Complete(()))?; // blank: not an entry
assert_eq!(session.len(), 1);Sourcepub fn is_empty(&self) -> bool
pub fn is_empty(&self) -> bool
Returns true if no entry has been committed yet.
§Examples
use repl_lang::Session;
assert!(Session::new().is_empty());Sourcepub fn entry(&self, number: u32) -> Option<&SourceFile>
pub fn entry(&self, number: u32) -> Option<&SourceFile>
The committed entry with the given number (counting from 1), or
None if there is no such entry.
The returned SourceFile gives the entry’s name (<repl:N>), its
text, and its global span — what a :history or :show command needs.
§Examples
use repl_lang::{Session, Status};
let mut session = Session::new();
session.feed("x = 1", |_| Status::Complete(()))?;
let first = session.entry(1).ok_or("missing")?;
assert_eq!(first.name(), "<repl:1>");
assert_eq!(first.text(), "x = 1\n");
assert!(session.entry(0).is_none());
assert!(session.entry(2).is_none());Sourcepub fn sources(&self) -> &SourceMap
pub fn sources(&self) -> &SourceMap
The source map holding every committed entry, one source per entry.
Render diagnostics against it: any span the pipeline produced from an
Input resolves to the right entry, line, and column.
§Examples
use diag_lang::{Diagnostic, Label, Renderer, Severity};
use repl_lang::{Session, Span, Status};
let mut session = Session::new();
let diag = session.feed("let y = x + 1", |input| {
// Pretend `x` is unbound: point at it with a global span.
let at = input.base().to_u32() + 8;
Status::Complete(Diagnostic::new(
Severity::Error,
"cannot find `x`",
Label::new(Span::new(at, at + 1), "not found"),
))
})?;
let repl_lang::Feed::Complete { value: diag, .. } = diag else { return Err("pending".into()) };
let text = Renderer::new().render(&diag, session.sources());
assert!(text.contains("<repl:1>:1:9"));
assert!(text.contains("^ not found"));