repl_lang/lib.rs
1//! # repl_lang
2//!
3//! The parts of a read-eval-print loop that every language needs and none
4//! should have to write twice: deciding when typed input forms a complete
5//! entry, keeping the session's entries so diagnostics can point back into
6//! them, and editing the line being typed.
7//!
8//! The crate owns no syntax and does no I/O. A language plugs in its own
9//! *pipeline* — lex, parse, evaluate — as a closure, and its own terminal or
10//! line source. repl-lang supplies the loop state between them:
11//!
12//! - [`Session`] accumulates lines into an entry, runs the pipeline on it after
13//! each line, and commits the entry once the pipeline reports
14//! [`Status::Complete`]. Every committed entry becomes a source named
15//! `<repl:N>` in one [`SourceMap`], so a `diag_lang` renderer can show any
16//! diagnostic from any entry.
17//! - [`Input`] is what the pipeline sees: the entry so far, its global position,
18//! a [`lexer_lang::Cursor`] already placed there, and
19//! [`is_incomplete`](Input::is_incomplete) — the "errors only at the end of
20//! input mean *keep typing*" test that turns any error-reporting parser into
21//! a continuation-line detector.
22//! - [`Editor`] is a terminal-agnostic line editor: cursor motion by visible
23//! character and by word, kill and yank, and a bounded history, driven by
24//! [`Edit`] commands the host maps from its own key events.
25//!
26//! ## Example
27//!
28//! A session over a fixed script, with a pipeline that asks for more input
29//! until brackets balance:
30//!
31//! ```
32//! use repl_lang::{Feed, Session, Status};
33//!
34//! let mut session = Session::new();
35//! let mut prompts = Vec::new();
36//!
37//! for line in ["sum([1, 2,", " 3, 4])", "len([])"] {
38//! prompts.push(if session.is_pending() { "..." } else { ">>>" });
39//! let feed = session.feed(line, |input| {
40//! let open = input.text().matches('[').count() + input.text().matches('(').count();
41//! let close = input.text().matches(']').count() + input.text().matches(')').count();
42//! if open > close { Status::Incomplete } else { Status::Complete(input.text().len()) }
43//! })?;
44//! if let Feed::Complete { entry, value } = feed {
45//! println!("entry {} complete: {value} bytes", entry.number());
46//! }
47//! }
48//!
49//! assert_eq!(prompts, [">>>", "...", ">>>"]);
50//! assert_eq!(session.len(), 2);
51//! assert_eq!(session.entry(1).map(|e| e.name()), Some("<repl:1>"));
52//! # Ok::<(), repl_lang::SessionError>(())
53//! ```
54//!
55//! ## Wiring a parser
56//!
57//! With a parser that reports a missing token at the end of input — as
58//! [`parser_lang`](https://docs.rs/parser-lang) does — the whole completeness
59//! check is one call:
60//!
61//! ```text
62//! session.feed(line, |input| {
63//! let tokens = lex(input.cursor()); // spans are already global
64//! let mut parser = Parser::new(&tokens);
65//! let program = parse_program(&mut parser);
66//! let errors = parser.into_errors();
67//! if input.is_incomplete(&errors) {
68//! Status::Incomplete // "1 + (2 *" — keep reading
69//! } else {
70//! Status::Complete((program, errors)) // done, right or wrong
71//! }
72//! })
73//! ```
74//!
75//! The `calc` example in the repository is a complete REPL built this way.
76//!
77//! ## Features
78//!
79//! - `std` (default) — the standard library. Without it the crate is
80//! `#![no_std]` and needs only `alloc`; neither the session nor the editor
81//! uses operating-system facilities.
82//!
83//! ## Stability
84//!
85//! The public surface is frozen and stable as of `1.0.0`: it follows Semantic
86//! Versioning, with no breaking changes before `2.0`. The surface, the
87//! behaviour that is part of the contract — entry naming and layout, the
88//! completeness rule, the history and display-unit rules — and the SemVer
89//! promise are catalogued in
90//! [`docs/API.md`](https://github.com/jamesgober/repl-lang/blob/main/docs/API.md#stability).
91
92#![cfg_attr(not(feature = "std"), no_std)]
93#![cfg_attr(docsrs, feature(doc_cfg))]
94#![forbid(unsafe_code)]
95#![deny(
96 warnings,
97 missing_docs,
98 unsafe_op_in_unsafe_fn,
99 unused_must_use,
100 unused_results,
101 clippy::unwrap_used,
102 clippy::expect_used,
103 clippy::panic,
104 clippy::todo,
105 clippy::unimplemented,
106 clippy::unreachable,
107 clippy::dbg_macro,
108 clippy::print_stdout,
109 clippy::print_stderr,
110 clippy::undocumented_unsafe_blocks
111)]
112
113extern crate alloc;
114
115mod edit;
116mod editor;
117mod error;
118mod feed;
119mod input;
120mod session;
121mod text;
122
123pub use edit::Edit;
124pub use editor::Editor;
125pub use error::SessionError;
126pub use feed::{Entry, Feed, Status};
127pub use input::Input;
128pub use session::Session;
129
130// Re-exported so the types in this crate's signatures can be named without
131// depending on the family crates directly.
132pub use diag_lang::Diagnostic;
133pub use lexer_lang::Cursor;
134pub use source_lang::{BytePos, SourceFile, SourceId, SourceMap, Span};
135
136/// Compiles and runs the `rust` code blocks in `README.md` and `docs/API.md` as
137/// part of `cargo test`, so the published examples cannot drift from the API.
138///
139/// Present only while collecting doctests (`#[cfg(doctest)]`); it is not part of
140/// the public surface and does not appear in the built library or its docs.
141#[cfg(doctest)]
142#[doc = include_str!("../README.md")]
143#[doc = include_str!("../docs/API.md")]
144pub struct MarkdownDocTests;