repl-lang 1.1.1

Read-eval-print loop scaffolding: line editing, session state, and incremental input feed.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
//! The view of pending input a session hands to the pipeline.

use diag_lang::{Diagnostic, Severity};
use lexer_lang::Cursor;
use source_lang::{BytePos, Span};

/// The pending text of one REPL entry, as the pipeline sees it.
///
/// Each time a line is fed to a [`Session`](crate::Session), the session calls
/// the pipeline with an `Input` holding *every* line of the entry so far —
/// the first line, then the first two, and so on until the pipeline answers
/// [`Status::Complete`](crate::Status::Complete). Lines are joined with `'\n'`
/// and the text always ends with one.
///
/// The input also carries its position in the session's
/// [`SourceMap`](source_lang::SourceMap): [`base`](Input::base) is the global
/// offset its first byte will occupy once the entry is committed. Lex with
/// [`cursor`](Input::cursor), or offset spans by `base` yourself, and every span
/// the pipeline produces — in tokens, in syntax trees, in diagnostics — is
/// already valid against [`Session::sources`](crate::Session::sources). There
/// is no second pass to rebase spans after the entry is accepted.
///
/// `Input` is a borrowed, `Copy` view; it lives only for the duration of the
/// pipeline call.
///
/// # Examples
///
/// ```
/// use repl_lang::{Feed, Session, Status};
///
/// let mut session = Session::new();
/// let feed = session.feed("1 + 2", |input| {
///     assert_eq!(input.text(), "1 + 2\n");
///     assert_eq!(input.number(), 1);
///     Status::Complete(input.text().len())
/// })?;
/// assert!(matches!(feed, Feed::Complete { value: 6, .. }));
/// # Ok::<(), repl_lang::SessionError>(())
/// ```
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Input<'a> {
    text: &'a str,
    base: u32,
    number: u32,
}

impl<'a> Input<'a> {
    /// Wraps pending text that the session has checked will fit at `base`.
    #[inline]
    pub(crate) const fn new(text: &'a str, base: u32, number: u32) -> Self {
        Self { text, base, number }
    }

    /// Every line of the entry so far, joined with `'\n'` and ending with one.
    ///
    /// # Examples
    ///
    /// ```
    /// use repl_lang::{Feed, Session, Status};
    ///
    /// let mut session = Session::new();
    /// let mut seen = Vec::new();
    /// for line in ["(1 +", " 2)"] {
    ///     session.feed(line, |input| {
    ///         seen.push(input.text().to_owned());
    ///         if input.text().matches('(').count() > input.text().matches(')').count() {
    ///             Status::Incomplete
    ///         } else {
    ///             Status::Complete(())
    ///         }
    ///     })?;
    /// }
    /// assert_eq!(seen, ["(1 +\n", "(1 +\n 2)\n"]);
    /// # Ok::<(), repl_lang::SessionError>(())
    /// ```
    #[inline]
    #[must_use]
    pub const fn text(&self) -> &'a str {
        self.text
    }

    /// The global position the first byte of this entry occupies in the
    /// session's source map.
    ///
    /// Entries are laid out end to end, so the base of entry *n* is the total
    /// length of entries 1 through *n − 1*.
    ///
    /// # Examples
    ///
    /// ```
    /// use repl_lang::{Session, Status};
    ///
    /// let mut session = Session::new();
    /// session.feed("abc", |_| Status::Complete(()))?; // occupies 0..4 ("abc\n")
    /// session.feed("x", |input| {
    ///     assert_eq!(input.base().to_u32(), 4);
    ///     Status::Complete(())
    /// })?;
    /// # Ok::<(), repl_lang::SessionError>(())
    /// ```
    #[inline]
    #[must_use]
    pub const fn base(&self) -> BytePos {
        BytePos::new(self.base)
    }

    /// The global span the whole entry will occupy: from
    /// [`base`](Input::base) to the end of [`text`](Input::text).
    ///
    /// # Examples
    ///
    /// ```
    /// use repl_lang::{Session, Span, Status};
    ///
    /// let mut session = Session::new();
    /// session.feed("let x = 1", |input| {
    ///     assert_eq!(input.span(), Span::new(0, 10));
    ///     Status::Complete(())
    /// })?;
    /// # Ok::<(), repl_lang::SessionError>(())
    /// ```
    #[inline]
    #[must_use]
    pub const fn span(&self) -> Span {
        // The session admits text only if `base + len` fits in `u32`.
        Span::new(self.base, self.base + self.text.len() as u32)
    }

    /// The entry number this input will be committed as, counting from `1`.
    ///
    /// Useful for prompts (`In [3]:`) and for names the evaluator binds per
    /// entry (`_3`). The number does not advance while an entry is incomplete,
    /// and blank lines never consume one.
    ///
    /// # Examples
    ///
    /// ```
    /// use repl_lang::{Session, Status};
    ///
    /// let mut session = Session::new();
    /// session.feed("a", |input| { assert_eq!(input.number(), 1); Status::Complete(()) })?;
    /// session.feed("b", |input| { assert_eq!(input.number(), 2); Status::Complete(()) })?;
    /// # Ok::<(), repl_lang::SessionError>(())
    /// ```
    #[inline]
    #[must_use]
    pub const fn number(&self) -> u32 {
        self.number
    }

    /// A [`lexer_lang::Cursor`] over [`text`](Input::text), positioned at
    /// [`base`](Input::base).
    ///
    /// Every token span the cursor produces is a global span in the session's
    /// source map, so tokens, the syntax built from them, and any diagnostic
    /// pointing at them render correctly against
    /// [`Session::sources`](crate::Session::sources) once the entry is
    /// committed.
    ///
    /// # Examples
    ///
    /// ```
    /// use repl_lang::{Session, Status};
    ///
    /// let mut session = Session::new();
    /// session.feed("first", |_| Status::Complete(()))?; // 0..6
    /// session.feed("ab", |input| {
    ///     let mut cursor = input.cursor();
    ///     cursor.eat_while(|c| c.is_alphabetic());
    ///     assert_eq!(cursor.lexeme(), "ab");
    ///     assert_eq!(cursor.token_span().start().to_u32(), 6); // global, not 0
    ///     Status::Complete(())
    /// })?;
    /// # Ok::<(), repl_lang::SessionError>(())
    /// ```
    #[inline]
    #[must_use]
    pub fn cursor(&self) -> Cursor<'a> {
        Cursor::with_base(self.text, self.base)
    }

    /// Returns `true` if `errors` say the input ended too soon rather than that
    /// it is wrong — the signal to ask for another line.
    ///
    /// That is the case when there is at least one error-severity diagnostic,
    /// and every one of them points at the end of the input: its primary span
    /// starts at or after the last non-whitespace byte. A parser that reaches
    /// the end of `1 + (2 *` reports "expected expression" *at the end*, so the
    /// user simply has not finished typing; a parser that trips over `1 + )`
    /// reports the error at the `)`, and more lines will never fix that.
    /// Warnings, notes, and help messages are ignored.
    ///
    /// The spans are **global** positions in the session's source map, as a
    /// parser fed by [`cursor`](Input::cursor) produces them. For a parser
    /// that runs on [`text`](Input::text) alone and reports positions from
    /// `0` (a lang-forge language, for one), use
    /// [`is_incomplete_relative`](Input::is_incomplete_relative) instead.
    ///
    /// The rule matches how [`parser_lang`] reports a missing token at the end
    /// of input — an empty span just past the last token — and works with any
    /// parser that does the same. Errors a lexer reports where a construct
    /// *starts*, such as an unterminated string pointing at its opening quote,
    /// are not recognised; return [`Status::Incomplete`](crate::Status::Incomplete)
    /// for those directly.
    ///
    /// [`parser_lang`]: https://docs.rs/parser-lang
    ///
    /// # Examples
    ///
    /// ```
    /// use diag_lang::{Diagnostic, Label, Severity};
    /// use repl_lang::{Session, Span, Status};
    ///
    /// let error_at = |at: u32| {
    ///     Diagnostic::new(Severity::Error, "expected expression", Label::new(Span::empty(at), "here"))
    /// };
    ///
    /// let mut session = Session::new();
    /// session.feed("1 + (2 *", |input| {
    ///     // An error at the end (byte 8, before the trailing newline): incomplete.
    ///     assert!(input.is_incomplete(&[error_at(8)]));
    ///     // An error in the middle: the input is wrong, not unfinished.
    ///     assert!(!input.is_incomplete(&[error_at(2), error_at(8)]));
    ///     // No errors at all: complete.
    ///     assert!(!input.is_incomplete(&[]));
    ///     Status::Complete(())
    /// })?;
    /// # Ok::<(), repl_lang::SessionError>(())
    /// ```
    #[must_use]
    pub fn is_incomplete(&self, errors: &[Diagnostic]) -> bool {
        // Fits: `base + text.len()` fits in `u32`, and this is no longer.
        errors_at_end(errors, self.base + self.meaningful_len())
    }

    /// [`is_incomplete`](Input::is_incomplete) for a parser whose spans are
    /// **relative to [`text`](Input::text)**: byte `0` is the entry's first
    /// byte, whatever its [`base`](Input::base).
    ///
    /// Use it when the pipeline parses `input.text()` on its own, without
    /// [`cursor`](Input::cursor) — a language forged with lang-forge, for
    /// example, whose `Language::parse(input.text())` reports positions from
    /// `0`. The rule is the same as `is_incomplete`'s: at least one
    /// error-severity diagnostic, and every one starting at or after the last
    /// non-whitespace byte of the text. Passing relative spans to
    /// `is_incomplete` instead would judge every entry after the first
    /// complete, because their end-of-input errors fall before the entry's
    /// global end.
    ///
    /// Which method suits which parser:
    ///
    /// | The parser… | Spans | Method |
    /// |---|---|---|
    /// | lexes with [`Input::cursor`] (or adds [`Input::base`] itself) | global | [`is_incomplete`](Input::is_incomplete) |
    /// | parses [`Input::text`] as a standalone string | relative | `is_incomplete_relative` |
    ///
    /// In the first entry the two agree, since `base` is `0`; from the second
    /// on, only the matching one is right.
    ///
    /// # Examples
    ///
    /// ```
    /// use diag_lang::{Diagnostic, Label, Severity};
    /// use repl_lang::{Session, Span, Status};
    ///
    /// let error_at = |at: u32| {
    ///     Diagnostic::new(Severity::Error, "expected expression", Label::new(Span::empty(at), "here"))
    /// };
    ///
    /// let mut session = Session::new();
    /// session.feed("let a = 1;", |_| Status::Complete(()))?; // occupies 0..11
    /// session.feed("(2 *", |input| {
    ///     assert_eq!(input.base().to_u32(), 11);
    ///     // A parser of `input.text()` alone reports the end of `(2 *` at 4.
    ///     assert!(input.is_incomplete_relative(&[error_at(4)]));
    ///     assert!(!input.is_incomplete_relative(&[error_at(1)])); // mid-text
    ///     // The same spans read as global would point into entry 1.
    ///     assert!(!input.is_incomplete(&[error_at(4)]));
    ///     Status::Complete(())
    /// })?;
    /// # Ok::<(), repl_lang::SessionError>(())
    /// ```
    #[must_use]
    pub fn is_incomplete_relative(&self, errors: &[Diagnostic]) -> bool {
        errors_at_end(errors, self.meaningful_len())
    }

    /// Length of the text without its trailing whitespace: where the last
    /// thing the user typed ends.
    #[inline]
    fn meaningful_len(&self) -> u32 {
        // Fits: the session admits text only if its length fits in `u32`.
        self.text.trim_end().len() as u32
    }
}

/// `true` if there is at least one error-severity diagnostic and every one
/// starts at or after `end`.
fn errors_at_end(errors: &[Diagnostic], end: u32) -> bool {
    let mut any = false;
    for error in errors.iter().filter(|d| d.severity() == Severity::Error) {
        if error.primary().span().start().to_u32() < end {
            return false;
        }
        any = true;
    }
    any
}

#[cfg(test)]
mod tests {
    use diag_lang::Label;

    use super::*;

    fn diag(severity: Severity, at: u32) -> Diagnostic {
        Diagnostic::new(severity, "m", Label::new(Span::empty(at), "l"))
    }

    #[test]
    fn test_span_covers_text_from_base() {
        let input = Input::new("ab\n", 10, 1);
        assert_eq!(input.span(), Span::new(10, 13));
        assert_eq!(input.base(), BytePos::new(10));
    }

    #[test]
    fn test_cursor_positions_are_global() {
        let input = Input::new("xy\n", 40, 1);
        let mut cursor = input.cursor();
        assert_eq!(cursor.pos(), BytePos::new(40));
        assert_eq!(cursor.bump(), Some('x'));
        assert_eq!(cursor.pos(), BytePos::new(41));
    }

    #[test]
    fn test_is_incomplete_no_errors_is_false() {
        assert!(!Input::new("x\n", 0, 1).is_incomplete(&[]));
    }

    #[test]
    fn test_is_incomplete_error_at_trimmed_end_is_true() {
        // "1 +   \n" — the meaningful text ends at 3.
        let input = Input::new("1 +   \n", 0, 1);
        assert!(input.is_incomplete(&[diag(Severity::Error, 3)]));
        assert!(input.is_incomplete(&[diag(Severity::Error, 7)]));
        assert!(!input.is_incomplete(&[diag(Severity::Error, 2)]));
    }

    #[test]
    fn test_is_incomplete_respects_base() {
        let input = Input::new("1 +\n", 100, 1);
        assert!(input.is_incomplete(&[diag(Severity::Error, 103)]));
        assert!(!input.is_incomplete(&[diag(Severity::Error, 3)]));
    }

    #[test]
    fn test_is_incomplete_ignores_non_errors() {
        let input = Input::new("1 +\n", 0, 1);
        assert!(!input.is_incomplete(&[diag(Severity::Warning, 3)]));
        assert!(input.is_incomplete(&[diag(Severity::Warning, 0), diag(Severity::Error, 3)]));
        assert!(!input.is_incomplete(&[diag(Severity::Note, 0), diag(Severity::Help, 1)]));
    }

    #[test]
    fn test_is_incomplete_any_early_error_wins() {
        let input = Input::new("a b c\n", 0, 1);
        let errors = [diag(Severity::Error, 5), diag(Severity::Error, 1)];
        assert!(!input.is_incomplete(&errors));
    }

    #[test]
    fn test_is_incomplete_relative_ignores_base() {
        // "1 +   \n" at base 100: the meaningful text ends at relative 3.
        let input = Input::new("1 +   \n", 100, 2);
        assert!(input.is_incomplete_relative(&[diag(Severity::Error, 3)]));
        assert!(input.is_incomplete_relative(&[diag(Severity::Error, 7)]));
        assert!(!input.is_incomplete_relative(&[diag(Severity::Error, 2)]));
        // Global positions are past the text, so they read as "at the end";
        // the global method is the one for them.
        assert!(input.is_incomplete(&[diag(Severity::Error, 103)]));
        assert!(!input.is_incomplete(&[diag(Severity::Error, 3)]));
    }

    #[test]
    fn test_is_incomplete_relative_same_rules_as_global() {
        let input = Input::new("a b c\n", 40, 3);
        assert!(!input.is_incomplete_relative(&[]));
        assert!(!input.is_incomplete_relative(&[diag(Severity::Warning, 5)]));
        let errors = [diag(Severity::Error, 5), diag(Severity::Error, 1)];
        assert!(!input.is_incomplete_relative(&errors));
        assert!(input.is_incomplete_relative(&[diag(Severity::Note, 0), diag(Severity::Error, 5)]));
    }

    #[test]
    fn test_both_methods_agree_at_base_zero() {
        let input = Input::new("x *\n", 0, 1);
        for at in 0..6 {
            let errors = [diag(Severity::Error, at)];
            assert_eq!(
                input.is_incomplete(&errors),
                input.is_incomplete_relative(&errors)
            );
        }
    }
}