Skip to main content

inillucent_cli/
lib.rs

1//! inillucent's command surface: the shell, the command table, and what reads them.
2//!
3//! Invariant: **there is one command table and every front end reads it.**
4//! A verb-shaped `inillucent` binary and an MCP server were added beside
5//! the `sqlite3`-shaped shell that was already here, and the reason all three
6//! live in one crate is that the alternative - a second list of commands, kept
7//! in step by whoever remembers - is the failure mode `drivers/README.md`
8//! describes for `java.sql.DatabaseMetaData`: a list nobody runs decays into a
9//! list of claims that were true once.
10//!
11//! So [`command::COMMANDS`] is the table, [`command::run`] executes an entry,
12//! and the three binaries are adapters over it:
13//!
14//! | binary | what it is |
15//! |---|---|
16//! | `inillucent-shell` | the `sqlite3`-shaped REPL. Unchanged by this ticket. |
17//! | `inillucent` | the verbs, for a script and for a person who is not in a REPL. |
18//! | `inillucent-mcp` | the same verbs as MCP tools, for an agent. |
19//!
20//! The layer below all of them is [`shell::Shell`], which is itself an adapter:
21//! every statement it runs goes through the public `inillucent` facade and it
22//! never reaches past it. The command table does not reach past the shell for
23//! the same reason - a third path to the same data is a third set of answers,
24//! and the difference is only ever found by somebody who trusted one of them.
25
26// **`deny` rather than `forbid`, for one file (task-1932, H11).**
27// `interrupt.rs` installs a console control handler so that Ctrl+C stops a
28// statement rather than the process, and there is no way to be told about
29// Ctrl+C in the standard library: both platforms offer one FFI call. Every
30// other file in this crate is still refused the word, `interrupt.rs` is
31// named in `policy.rs`'s `UNSAFE_ALLOWED`, and both of its `unsafe` blocks
32// carry their own SAFETY note.
33#![deny(unsafe_code)]
34#![deny(missing_docs)]
35#![deny(clippy::indexing_slicing)]
36#![deny(clippy::unwrap_used)]
37#![deny(clippy::expect_used)]
38#![deny(clippy::panic)]
39#![cfg_attr(
40    test,
41    allow(
42        clippy::expect_used,
43        clippy::indexing_slicing,
44        clippy::panic,
45        clippy::unwrap_used
46    )
47)]
48
49/// How much stack a statement is given, in bytes.
50///
51/// **A statement the parser accepts must not overflow any later stage
52/// (task-1979, section 5.3).** `SELECT abs(abs(...(1)...))` 300 deep ended the
53/// process with `thread 'main' has overflowed its stack` and exit code
54/// 0xC00000FD, in debug and in release, on the command line and on the MCP
55/// server - where it ends the server for every client. The declared limits are
56/// SQLite's own, `ExprDepth` 1000 and `ParserDepth` 2500, and the binaries
57/// carry a 1 MiB stack reserve read from the PE header, so the limit could
58/// never be the thing that fired.
59///
60/// **Measured on this build rather than guessed.** Nesting `abs()` on the
61/// default 1 MiB main thread overflows between 34 and 38 levels in a debug
62/// build, so one level of parse, bind, plan and execute costs about 29 KiB
63/// there - the deepest and most expensive of the two builds, which is the one
64/// to size against. `ExprDepth` at 1000 therefore needs about 29 MiB, and 64
65/// MiB is that with a margin of more than two.
66///
67/// **Reserved, not spent.** A thread's stack is reserved address space that
68/// the operating system commits a page at a time as it is used, on Windows and
69/// on Linux alike, so a program that never writes a deep expression pays for
70/// none of this. Raising the *limits* instead would move the crash rather than
71/// remove it, which is why the stack is sized to the limit and not the other
72/// way round.
73pub const STATEMENT_STACK: usize = 64 << 20;
74
75/// Runs a program's whole body on a thread with [`STATEMENT_STACK`] bytes of
76/// stack, and returns what it produced.
77///
78/// **The whole body rather than one request**, because the engine is `!Send`:
79/// a `Database` holds `Rc`s and cannot be moved to a thread once it exists, so
80/// the thread is taken first and the database is opened on it. What that buys
81/// is the same thing per-request threads would: the depth limits are against a
82/// stack this crate chose rather than against whatever the linker's default
83/// reserve happened to be.
84///
85/// **A plain function rather than a closure**, so that a machine which cannot
86/// start a thread can still run the body where it is - `Builder::spawn`
87/// consumes a closure whether or not it succeeds, and a function pointer is
88/// `Copy`. The three programs that call this each hand it their own `run`.
89///
90/// @param body - what to run
91pub fn on_a_sized_stack<R: Send + 'static>(body: fn() -> R) -> R {
92    match std::thread::Builder::new()
93        .stack_size(STATEMENT_STACK)
94        .spawn(body)
95    {
96        Ok(running) => match running.join() {
97            Ok(produced) => produced,
98            // A panic on the worker is the panic the caller would have had, so
99            // it is re-raised here rather than turned into a value nobody
100            // expects.
101            Err(panicked) => std::panic::resume_unwind(panicked),
102        },
103        // A machine that cannot start a thread runs the body where it is. The
104        // limits are then against the default reserve, which is what they were
105        // against before this existed.
106        Err(_) => body(),
107    }
108}
109
110pub mod archive;
111pub mod command;
112pub mod commands;
113pub mod dbconfig;
114pub mod diagnose;
115pub mod dot;
116pub mod dump;
117pub mod help;
118pub mod import;
119// The one module allowed the word, and only for the two calls that install a
120// console control handler. See its own header.
121#[allow(unsafe_code)]
122pub mod interrupt;
123pub mod json;
124pub mod mcp;
125pub mod render;
126pub mod setup;
127pub mod shell;