Skip to main content

Session

Struct Session 

Source
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

Source

pub const DEFAULT_LIMIT: usize

The entry size limit of Session::new: 1 MiB.

Source

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);
Source

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());
Source

pub fn feed<T, F>( &mut self, line: &str, pipeline: F, ) -> Result<Feed<T>, SessionError>
where F: FnOnce(Input<'_>) -> Status<T>,

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 line is blank, nothing is evaluated: the result is Feed::Empty and the pipeline is not called.
  • Otherwise the line joins the pending entry and the pipeline is called with an Input over all of it.
  • On Status::Incomplete the entry stays pending and the result is Feed::Incomplete. Blank lines inside an entry are passed through, so a language can use one to end a block.
  • On Status::Complete the entry is committed as <repl:N> and the result is Feed::Complete with the pipeline’s value. The pending buffer is cleared for the next entry.
§Errors

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);
Source

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());
Source

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), "... ");
Source

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");
Source

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);
Source

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());
Source

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());
Source

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"));
Source

pub fn limit(&self) -> usize

The maximum byte length of one entry.

§Examples
use repl_lang::Session;

assert_eq!(Session::with_limit(4096).limit(), 4096);

Trait Implementations§

Source§

impl Clone for Session

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Session

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for Session

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.