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