irgx/runtime/mod.rs
1//! Transports, the fallback ladder, and the crate's typed failures.
2//!
3//! Every face of this crate asks its question here, and `runtime` decides *how*
4//! it gets answered:
5//!
6//! | tier | when |
7//! |---|---|
8//! | [`plane`] — the in-process analytic C ABI | the symbols are present and the schema digest agrees |
9//! | [`relay`] — the certified CLIs, NDJSON in | always; the fail-open floor |
10//! | [`shell`] / [`session`] — the exact plane's process + resident transports | the rg-parity search surface |
11//!
12//! The ladder is **fail-open by construction**. `IRGX_STALE` is a
13//! *declinature*, not a failure: the tier is saying "ask the next one and you
14//! will get the same answer", so it never reaches the caller as an `Err`. The
15//! same is true of an absent analytic symbol — an engine built before the analytic plane
16//! landed simply has no in-process plane, and the crate keeps working.
17//!
18//! Exactly two things are loud. A **schema digest mismatch** ([`Error::SchemaDrift`])
19//! means the library's row tables and this build's decoder disagree, so falling
20//! back would hide a real version skew; and a **row that contradicts its own
21//! declaration** ([`Error::Decode`]) is corruption, not a miss.
22
23pub mod answer;
24mod cancel;
25pub mod cell;
26pub mod decode;
27pub mod handshake;
28mod lower;
29pub mod plane;
30mod readout;
31pub mod relay;
32#[cfg(unix)]
33pub mod session;
34pub mod shell;
35pub mod sys;
36mod verify;
37
38/// Synthesized wire buffers the decoder tests are driven from.
39#[cfg(test)]
40mod fixture;
41
42#[cfg(unix)]
43pub use session::{Session, default_socket_path, warm_eligible};
44
45use std::fmt;
46use std::os::raw::c_void;
47
48pub use answer::{Batch, BatchIter, RowIter, Rows, Stats, Tier};
49pub use cancel::CancelToken;
50pub use cell::{OwnedRow, OwnedValue, RowSeq, Texts, Value};
51pub use decode::Row;
52
53/// The one error type every fallible call in this crate returns.
54#[derive(Debug)]
55#[non_exhaustive]
56pub enum Error {
57 /// No engine binary could be located: not at the per-face env override
58 /// (`<FACE>_BIN`), not at a built `zig-out/bin/<name>` in
59 /// this checkout, an ancestor, or the sibling checkout that owns the name,
60 /// and not on `PATH`. The message names every path it tried, in order.
61 /// Build one with `zig build`.
62 NotFound(String),
63 /// The pattern or flag combination is outside the linear-time engine
64 /// (e.g. PCRE2 lookaround/backreferences, `-U` multiline) — the engine
65 /// exited 2 and named the ripgrep fallback on stderr.
66 UnsupportedPattern(String),
67 /// The pattern is malformed in EVERY grammar the engine has, so no `engine`
68 /// choice lifts it — the message names the defect and points at the
69 /// offending byte. Distinct from [`Error::UnsupportedPattern`] because the
70 /// two ask for opposite responses: that one says *retry on another engine*,
71 /// this one says *fix the pattern*. The engine only makes this claim after
72 /// asking PCRE2 and being refused too.
73 BadPattern(String),
74 /// The engine exited non-zero for an I/O, walk, or timeout reason (an
75 /// unreadable directory, a missing explicit path) — fail-loud, never a
76 /// silent empty result.
77 Failed(String),
78 /// A [`crate::SearchRequest`] option the in-process cursor ABI cannot honor
79 /// (glob/type scoping, multiline, `no_index`, a non-linear `engine`, …). The
80 /// in-process `Engine` carries only match-finding intent the C ABI has a
81 /// field for; run the full CLI surface through [`crate::SearchRequest::run`]
82 /// instead.
83 Unrepresentable(String),
84 /// The loaded library's row-schema digest disagrees with the table this
85 /// crate's decoder was generated from. Named down to the drifting schema,
86 /// because the alternative is a silently mis-decoded row.
87 SchemaDrift(String),
88 /// A row contradicted its own declaration — an unknown schema id, a value
89 /// tag disagreeing with the contract, or text that is not UTF-8.
90 Decode(String),
91 /// This process has no in-process analytic plane, or its engine predates the
92 /// cancellation trio, so there is no query a token could stop. Distinct from
93 /// [`Error::Failed`] because it is a statement about the build rather than a
94 /// fault: every verb still answers, through the subprocess tier, and a host
95 /// that does not need to interrupt one can ignore this entirely.
96 Uncancellable,
97 /// The child process could not be spawned or its pipes could not be read.
98 Io(std::io::Error),
99}
100
101/// Crate-wide `Result` alias.
102pub type Result<T> = std::result::Result<T, Error>;
103
104impl fmt::Display for Error {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 // The message already names the tool it went looking for, and it is
108 // not always the exact face — the same error carries a missing
109 // kinship or composed binary. Prefix with the crate, not with one
110 // of the three faces.
111 Self::NotFound(m) => write!(f, "irregex: {m}"),
112 Self::UnsupportedPattern(m) => write!(f, "unsupported pattern: {m}"),
113 Self::BadPattern(m) => write!(f, "malformed pattern: {m}"),
114 Self::Failed(m) => write!(f, "gist search failed: {m}"),
115 Self::Unrepresentable(m) => write!(f, "option not representable in-process: {m}"),
116 Self::SchemaDrift(m) => write!(f, "analytic schema drift: {m}"),
117 Self::Decode(m) => write!(f, "analytic row does not match its schema: {m}"),
118 Self::Uncancellable => write!(
119 f,
120 "this process has no in-process analytic plane to cancel; \
121 every verb still answers through the subprocess tier"
122 ),
123 Self::Io(e) => write!(f, "gist io error: {e}"),
124 }
125 }
126}
127
128impl std::error::Error for Error {
129 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
130 match self {
131 Self::Io(e) => Some(e),
132 _ => None,
133 }
134 }
135}
136
137impl From<std::io::Error> for Error {
138 fn from(e: std::io::Error) -> Self {
139 Self::Io(e)
140 }
141}
142
143// ── the request seam ───────────────────────────────────────────────────────
144
145/// One analytic request, lowered for whichever tier answers it.
146///
147/// Each of the five `[analytic.params]` families implements this once; the two
148/// transports then consume the *same* request without either knowing the other
149/// exists. Adding a verb to an existing family costs one [`Query::op`] arm.
150pub trait Query {
151 /// The `IRGX_OP_*` code (`[analytic.verbs]`).
152 fn op(&self) -> u32;
153 /// The size-checked C params struct, borrowing this request's buffers.
154 fn wire(&self) -> Wire<'_>;
155 /// The CLI invocation that answers the same question out of process.
156 ///
157 /// # Errors
158 /// [`Error::Unrepresentable`] for a request the CLI surface cannot spell.
159 fn argv(&self) -> Result<relay::Invocation>;
160 /// The corpus roots. The in-process plane carries them on the *engine*
161 /// (there is no roots field in any params family), the CLIs carry them as
162 /// trailing argv — so the request declares them once, here.
163 fn roots(&self) -> &[std::path::PathBuf];
164 /// The pattern set for the two families that carry one (`sweep`, `compose`).
165 ///
166 /// Kept off [`Query::wire`] because an `irgx_text[]` has to be built
167 /// somewhere that outlives the params struct, and the only such place is the
168 /// caller of both.
169 fn texts(&self) -> Vec<&str> {
170 Vec::new()
171 }
172 /// The directory to run a subprocess in; `None` = inherit.
173 fn cwd(&self) -> Option<&std::path::Path> {
174 None
175 }
176}
177
178/// A filled `[analytic.params]` family, borrowing the request that built it.
179///
180/// The families are separate structs (rather than one union) because a
181/// producer entry size-checks each against its declared shape, and a
182/// mismatched size is `IRGX_INVALID` by design.
183pub enum Wire<'a> {
184 Kinship(sys::KinshipParams, std::marker::PhantomData<&'a ()>),
185 Retrieval(sys::RetrievalParams, std::marker::PhantomData<&'a ()>),
186 Sweep(sys::SweepParams, std::marker::PhantomData<&'a ()>),
187 Compose(sys::ComposeParams, std::marker::PhantomData<&'a ()>),
188 Rank(sys::RankParams, std::marker::PhantomData<&'a ()>),
189}
190
191impl<'a> Wire<'a> {
192 /// Point the two pattern-carrying families at a `irgx_text[]` the caller
193 /// owns. `'a` ties that array to the same request the params borrow from, so
194 /// the pointer cannot outlive the strings behind it.
195 pub fn bind(&mut self, texts: &'a [sys::Text]) {
196 let (ptr, n) = (texts.as_ptr(), texts.len());
197 match self {
198 Self::Sweep(p, _) => (p.patterns, p.npatterns) = (ptr, n),
199 Self::Compose(p, _) => (p.patterns, p.npatterns) = (ptr, n),
200 _ => {},
201 }
202 }
203
204 /// The opaque pointer a producer entry expects, valid for `&self`.
205 pub fn as_ptr(&self) -> *const c_void {
206 match self {
207 Self::Kinship(p, _) => std::ptr::from_ref(p).cast(),
208 Self::Retrieval(p, _) => std::ptr::from_ref(p).cast(),
209 Self::Sweep(p, _) => std::ptr::from_ref(p).cast(),
210 Self::Compose(p, _) => std::ptr::from_ref(p).cast(),
211 Self::Rank(p, _) => std::ptr::from_ref(p).cast(),
212 }
213 }
214}
215
216/// `struct_size` for a params family — the fail-closed handshake every
217/// `[analytic.params]` struct opens with.
218pub fn struct_size<T>() -> u32 {
219 u32::try_from(std::mem::size_of::<T>()).unwrap_or(u32::MAX)
220}
221
222/// Answer one analytic request, walking the ladder.
223///
224/// # Errors
225/// Propagates [`Error::SchemaDrift`] and [`Error::Decode`] loud; every other
226/// in-process refusal falls through to the subprocess tier.
227pub fn answer(query: &impl Query) -> Result<Rows> {
228 match plane::run(query)? {
229 Some(rows) => Ok(rows),
230 None => relay::run(query),
231 }
232}