repl_lang/feed.rs
1//! The values exchanged between a session and its pipeline.
2
3use source_lang::{SourceId, Span};
4
5/// The pipeline's verdict on the pending input: finished, or waiting for more.
6///
7/// The pipeline closure passed to [`Session::feed`](crate::Session::feed)
8/// returns a `Status`. `Complete` carries whatever the pipeline produced — a
9/// parsed tree, an evaluated value, a list of diagnostics, or a `Result` of
10/// those — and the session hands it straight back in
11/// [`Feed::Complete`]. Because the check for completeness and the real work
12/// are the same call, the pipeline parses each finished entry exactly once.
13///
14/// Return `Complete` for input that is *wrong* as well as input that is
15/// right: a syntax error in the middle of a line will not be fixed by typing
16/// more lines. Only input that ended too soon should be `Incomplete`; see
17/// [`Input::is_incomplete`](crate::Input::is_incomplete) for the usual test.
18///
19/// # Examples
20///
21/// ```
22/// use repl_lang::{Feed, Session, Status};
23///
24/// // A pipeline that treats a trailing backslash as a line continuation.
25/// let pipeline = |input: repl_lang::Input<'_>| {
26/// if input.text().trim_end().ends_with('\\') {
27/// Status::Incomplete
28/// } else {
29/// Status::Complete(input.text().lines().count())
30/// }
31/// };
32///
33/// let mut session = Session::new();
34/// assert!(matches!(session.feed("echo one \\", pipeline)?, Feed::Incomplete));
35/// assert!(matches!(session.feed("two", pipeline)?, Feed::Complete { value: 2, .. }));
36/// # Ok::<(), repl_lang::SessionError>(())
37/// ```
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
39pub enum Status<T> {
40 /// The input is a whole entry. The session commits it and returns the
41 /// value in [`Feed::Complete`].
42 Complete(T),
43 /// The input stops partway through a construct. The session keeps it and
44 /// calls the pipeline again with the next line appended.
45 Incomplete,
46}
47
48/// The outcome of feeding one line to a [`Session`](crate::Session).
49///
50/// A host's read loop matches on it: evaluate and print on `Complete`, show a
51/// continuation prompt on `Incomplete`, and simply prompt again on `Empty`.
52///
53/// # Examples
54///
55/// ```
56/// use repl_lang::{Feed, Session, Status};
57///
58/// let mut session = Session::new();
59/// let pipeline = |input: repl_lang::Input<'_>| {
60/// let open = input.text().matches('{').count();
61/// let close = input.text().matches('}').count();
62/// if open > close { Status::Incomplete } else { Status::Complete(input.text().len()) }
63/// };
64///
65/// let mut transcript = Vec::new();
66/// for line in ["", "f {", " x", "}"] {
67/// let prompt = if session.is_pending() { "... " } else { ">>> " };
68/// match session.feed(line, pipeline)? {
69/// Feed::Complete { entry, value } => {
70/// transcript.push(format!("{prompt}{line} -> entry {} ({value} bytes)", entry.number()));
71/// }
72/// Feed::Incomplete => transcript.push(format!("{prompt}{line}")),
73/// Feed::Empty => transcript.push(prompt.trim_end().to_owned()),
74/// }
75/// }
76/// assert_eq!(transcript, [">>>", ">>> f {", "... x", "... } -> entry 1 (10 bytes)"]);
77/// # Ok::<(), repl_lang::SessionError>(())
78/// ```
79#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
80pub enum Feed<T> {
81 /// The pipeline accepted the entry. It is now committed to the session's
82 /// source map, so diagnostics carrying its spans render against
83 /// [`Session::sources`](crate::Session::sources).
84 Complete {
85 /// Where the committed entry lives.
86 entry: Entry,
87 /// What the pipeline returned in [`Status::Complete`].
88 value: T,
89 },
90 /// The pipeline needs more input. The line is kept, and the next one is
91 /// appended to it.
92 Incomplete,
93 /// The line was blank and no entry was in progress, so there was nothing
94 /// to evaluate. The pipeline was not called and no entry number was used.
95 Empty,
96}
97
98/// A committed entry: its number and where its text lives in the session's
99/// source map.
100///
101/// Returned in [`Feed::Complete`]. The text itself is stored once, in the
102/// session; an `Entry` is a small `Copy` handle to it.
103///
104/// # Examples
105///
106/// ```
107/// use repl_lang::{Feed, Session, Span, Status};
108///
109/// let mut session = Session::new();
110/// session.feed("first", |_| Status::Complete(()))?;
111/// let Feed::Complete { entry, .. } = session.feed("second", |_| Status::Complete(()))? else {
112/// return Err("the pipeline always completes".into());
113/// };
114///
115/// assert_eq!(entry.number(), 2);
116/// assert_eq!(entry.span(), Span::new(6, 13));
117/// let source = session.sources().source(entry.id()).ok_or("missing")?;
118/// assert_eq!(source.name(), "<repl:2>");
119/// assert_eq!(source.text(), "second\n");
120/// # Ok::<(), Box<dyn std::error::Error>>(())
121/// ```
122#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
123pub struct Entry {
124 number: u32,
125 id: SourceId,
126 span: Span,
127}
128
129impl Entry {
130 #[inline]
131 pub(crate) const fn new(number: u32, id: SourceId, span: Span) -> Self {
132 Self { number, id, span }
133 }
134
135 /// The entry's number, counting from `1` in the order entries were
136 /// committed. It matches [`Input::number`](crate::Input::number) for the
137 /// input that produced it and is the key for
138 /// [`Session::entry`](crate::Session::entry).
139 #[inline]
140 #[must_use]
141 pub const fn number(&self) -> u32 {
142 self.number
143 }
144
145 /// The entry's id in [`Session::sources`](crate::Session::sources).
146 #[inline]
147 #[must_use]
148 pub const fn id(&self) -> SourceId {
149 self.id
150 }
151
152 /// The global span of the entry's text, equal to the
153 /// [`Input::span`](crate::Input::span) the pipeline saw.
154 #[inline]
155 #[must_use]
156 pub const fn span(&self) -> Span {
157 self.span
158 }
159}