quarb 0.33.0

Query engine for arbors (arboreal graphs - tree-spanned or tree-dominated graphs)
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
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
//! The Quarb query engine.
//!
//! Quarb is a query language for *arbors* — tree-spanned graphs. This
//! crate is the engine: it lexes and parses a query, then evaluates
//! it against an [`AstAdapter`] that maps some data source onto the
//! arbor model.
//!
//! It currently implements tree navigation in full (child, descendant,
//! parent, ancestor, and sibling hops; proximal/distal reach; root and
//! leaf anchors; literal, glob, and `~(...)` regex name matching;
//! `<trait>` filters), scalar projection (`::` properties, `:::` core
//! metadata, `;;;` adapter metadata), `[...]` predicates (comparisons,
//! `and`/`or`/`not`/`!`, structural conditions, index selection),
//! `||` union, `->` / `<-` crosslink navigation, path patterns
//! (`(...)` groups with subpath alternation and `+`/`*`/`{m,n}`
//! quantifiers — simple-path expansion under the adapter's
//! [quantifier bound](AstAdapter::quantifier_bound)), and the register
//! system: per-capsa `| f` transforms, `@| f` whole-context
//! aggregation, `| .` push and `| $.` recall of breadcrumbs, and
//! `| .(expr)` subcontexts for grouped aggregation. It also does
//! correlation: `E1 <=> E2[…$*1…]` joins two contexts, a predicate
//! referencing a prior expression's context via `$*N`. It also
//! resolves cross-references: `::prop-->` maps a reference to its
//! target node (a JSON adapter follows a `$ref` JSON Pointer). The
//! reverse resolution `<--` and pattern search `=>` are not built yet.
//! See `doc/impl.tex`.
//!
//! ```
//! # use quarb::{AstAdapter, NodeId, QueryResult, run};
//! # struct Empty;
//! # impl AstAdapter for Empty {
//! #     fn root(&self) -> NodeId { NodeId(0) }
//! #     fn children(&self, _: NodeId) -> Vec<NodeId> { vec![] }
//! #     fn name(&self, _: NodeId) -> Option<String> { None }
//! # }
//! let hits = run("//*.rs", &Empty).unwrap();
//! assert!(matches!(hits, QueryResult::Nodes(ns) if ns.is_empty()));
//! ```

pub mod adapter;
mod ast;
pub mod bibref;
pub mod datenames;
mod encoding;
mod error;
mod exec;
pub mod kaiv_out;
mod lexer;
pub mod locale;
pub mod number;
mod parser;
pub mod reflect;
pub mod source;
pub mod trace;
pub use encoding::{base64, base64_decode, sha256, sha256_hex};
pub mod complete;
pub mod highlight;
pub mod quantity;
mod stdlib;
pub use stdlib::{tabulate, tabulate_in, tabulate_with};
pub mod period;
pub mod target;
pub mod temporal;
pub mod timex;
mod timexdata;
pub mod translit;
mod unparse;
mod value;

pub use adapter::{
    AllowShell, AstAdapter, ExternalRef, InstantFrom, LinkDir, NodeId, Provenance, ProvenanceList,
    QuantifierBound, WithNow,
};
pub use error::{QuarbError, Result};
pub mod koine;

/// The invocation instant, pinned once by the tool layer (`qua
/// --now`, or the clock read once at startup) and read by
/// adapters that would otherwise consult the wall clock when
/// resolving a relative window (`since=30m`). A pinned run
/// replays: evaluation never reads a clock, and neither does a
/// mount. `None` before the tool sets it — a library embedder
/// that never pins gets the old wall-clock behavior.
mod pinned {
    use std::cell::Cell;
    thread_local! {
        pub(super) static NOW: Cell<Option<(i64, u32)>> = const { Cell::new(None) };
    }
}

/// Pin the invocation instant for this thread (seconds and nanos
/// since the epoch). Called once, by the tool, before mounting.
pub fn set_invocation_instant(secs: i64, nanos: u32) {
    pinned::NOW.with(|c| c.set(Some((secs, nanos))));
}

thread_local! {
    /// The session's sentence bonds (a `.desm` set):
    /// what `sentences`, `sc` and the corpus reading segment with,
    /// so a pipeline and a `corpus:` mount agree on "Dr. Smith".
    static SENTENCE_BONDS: std::cell::RefCell<Option<std::rc::Rc<syndesmos::Syndesmos>>> =
        const { std::cell::RefCell::new(None) };
}

/// Set the session's sentence bonds (`qua --desm`, quai's).
pub fn set_sentence_bonds(bonds: Option<std::rc::Rc<syndesmos::Syndesmos>>) {
    SENTENCE_BONDS.with(|b| *b.borrow_mut() = bonds);
}

/// The session's sentence bonds, if any were loaded.
pub fn sentence_bonds() -> Option<std::rc::Rc<syndesmos::Syndesmos>> {
    SENTENCE_BONDS.with(|b| b.borrow().clone())
}

/// The pinned invocation instant, if the tool layer set one.
pub fn invocation_instant() -> Option<(i64, u32)> {
    pinned::NOW.with(|c| c.get())
}

/// The pinned instant in whole seconds, falling back to the wall
/// clock — the one call an adapter needs when resolving a
/// relative window. Reading it in `open` (never during
/// evaluation) keeps a mount as reproducible as a query.
pub fn now_secs() -> i64 {
    match invocation_instant() {
        Some((secs, _)) => secs,
        None => std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_secs() as i64)
            .unwrap_or(0),
    }
}
pub use exec::{
    ORIGIN_CAP, Origin, Origins, Prov, QueryResult, Traced, canonical_identity, fetched,
    fetched_opt, is_url, name_path, record_refusal, resolved_provenance, set_ref_targets,
    set_reproducible, take_refs,
};
pub use parser::{Defs, parse_scope_path};
pub use value::Value;

/// The universal search over a step body: `*//BODY`, with the
/// wildcard supplied when the body opens with a trait expression or
/// a predicate (`trait <long-text>: [:: | cc > 300];` means every
/// node that passes).
pub fn step_body_query(body: &str) -> String {
    let body = body.trim();
    if body.starts_with(['<', '[']) {
        format!("*//*{body}")
    } else {
        format!("*//{body}")
    }
}

/// One path step's body, parsed — a name, a pattern, an
/// alternation, a trait expression, predicates, in any combination,
/// without the navigation operator — as a model's `trait <name>:
/// BODY;` declaration takes it. Test a node against it with
/// [`node_matches_step_body`]: decided at the node, no walk.
pub struct StepBody(ast::PathElem);

/// Parse `body` as one step's body; refuses anything beyond that
/// single step, and positional predicates (`[1]`, `[2..3]`), which
/// have no meaning at a single node.
pub fn parse_step_body(body: &str) -> Result<StepBody> {
    let q = parser::parse(&lexer::lex(&step_body_query(body))?)?;
    let refuse = |what: &str| {
        Err(QuarbError::Parse(format!(
            "a trait declaration takes one step's body (a name, a pattern, \
             a trait expression, predicates), not {what}: '{body}'"
        )))
    };
    if !q.correlations.is_empty() || !q.pipeline.is_empty() {
        return refuse("a pipeline");
    }
    if q.branches.len() != 1 || q.branches[0].projection.is_some() {
        return refuse("a projection or a union");
    }
    if q.branches[0].steps.len() != 1 {
        return refuse("a path");
    }
    let elem = q
        .branches
        .into_iter()
        .next()
        .unwrap()
        .steps
        .into_iter()
        .next()
        .unwrap();
    let preds = match &elem {
        ast::PathElem::Step(s) => &s.predicates,
        ast::PathElem::Group(g) => &g.predicates,
        _ => return refuse("that"),
    };
    if preds
        .iter()
        .any(|p| matches!(p, ast::Predicate::Index(_) | ast::Predicate::Range(_, _)))
    {
        return refuse("a positional predicate");
    }
    Ok(StepBody(elem))
}

/// Check that `body` parses as one step's body (see
/// [`parse_step_body`]).
pub fn validate_step_body(body: &str) -> Result<()> {
    parse_step_body(body).map(|_| ())
}

/// Whether `node` would match the step body as a step of a path:
/// decided at the node, the predicates evaluated there, no hop.
pub fn node_matches_step_body(adapter: &impl AstAdapter, node: NodeId, body: &StepBody) -> bool {
    exec::node_matches_elem(adapter, node, &body.0)
}

/// Convert a refusal recorded during evaluation into an error. The
/// eval layer below the entry points has no error channel; a walk
/// that would return a silently incomplete answer (e.g. a
/// quantifier cut off at the bound) records a refusal instead, and
/// the entry points surface it here.
fn checked<T>(result: T) -> Result<T> {
    match exec::take_refusal() {
        Some(msg) => Err(QuarbError::Refused(msg)),
        None => Ok(result),
    }
}

/// Lex, parse, and evaluate `query` against `adapter`.
///
/// Returns the [`QueryResult`] — a node set, or scalar values if the
/// query ends in a projection — or an error if the query is malformed
/// or uses a feature the engine does not implement yet.
pub fn run(query: &str, adapter: &impl AstAdapter) -> Result<QueryResult> {
    let tokens = lexer::lex(query)?;
    let ast = parser::parse_with_data(&tokens, Defs::default(), Some(adapter))?;
    exec::gate_shell(&ast, adapter)?;
    checked(exec::eval(&ast, adapter))
}

/// A query's closing serialization for people — `@| csv` or
/// `@| table` as the last stage — taken off for the tool to render.
#[derive(Debug, Clone)]
pub struct ClosingTable {
    pub csv: bool,
    /// The named columns, in order; empty for every field.
    pub columns: Vec<String>,
    /// The delimiter `csv(";")` names, when it names one.
    pub delimiter: Option<u8>,
}

/// Like [`run`], for a tool that renders under an output locale:
/// when the query closes with `@| csv` or `@| table`, the rows are
/// returned as they stand before that stage, with its shape, and the
/// tool tabulates them itself (`tabulate_in` with its locale's cells
/// and delimiter). The stage anywhere else, and every other stage,
/// yields the same values as under [`run`] — a value never depends
/// on a locale.
pub fn run_for_rendering(
    query: &str,
    adapter: &impl AstAdapter,
) -> Result<(QueryResult, Option<ClosingTable>)> {
    let tokens = lexer::lex(query)?;
    let mut ast = parser::parse_with_data(&tokens, Defs::default(), Some(adapter))?;
    let mut closing = None;
    if let Some(ast::Stage::Agg(call)) = ast.pipeline.last_mut()
        && matches!(call.name.as_str(), "csv" | "table")
    {
        let (delimiter, columns) = stdlib::split_csv_delimiter(call);
        closing = Some(ClosingTable {
            csv: call.name == "csv",
            columns: columns
                .iter()
                .filter_map(|a| match a {
                    ast::Arg::Lit(Value::Str(s)) => Some(s.clone()),
                    _ => None,
                })
                .collect(),
            delimiter,
        });
        // The rows, with a node standing for the record of the
        // columns asked of it, exactly as the stage would read them.
        let columns: Vec<ast::Arg> = columns.to_vec();
        call.name = stdlib::ROWS_FOR_RENDERING.to_string();
        call.args = columns;
    }
    exec::gate_shell(&ast, adapter)?;
    Ok((checked(exec::eval(&ast, adapter))?, closing))
}

/// Like [`run`], but returning the final capsae as `(node, topic)`
/// pairs, so each result value still knows which node produced it —
/// the provenance-bearing form used by typed emission (`qua --daiv`).
/// A `None` topic is a node result.
pub fn run_traced(query: &str, adapter: &impl AstAdapter) -> Result<Vec<(NodeId, Option<Value>)>> {
    let tokens = lexer::lex(query)?;
    let ast = parser::parse_with_data(&tokens, Defs::default(), Some(adapter))?;
    exec::gate_shell(&ast, adapter)?;
    checked(exec::eval_traced(&ast, adapter))
}

/// Like [`run_traced`], with each value's provenance: the origins
/// of its topic, per field for a record ([`exec::Traced`]).
pub fn run_traced_prov(query: &str, adapter: &impl AstAdapter) -> Result<Vec<exec::Traced>> {
    let tokens = lexer::lex(query)?;
    let ast = parser::parse_with_data(&tokens, Defs::default(), Some(adapter))?;
    exec::gate_shell(&ast, adapter)?;
    checked(exec::eval_traced_prov(&ast, adapter))
}

/// Parse a definitions file — `def &name(params): body;` statements
/// only — into a fragment table for [`run_with_defs`]. Lines whose
/// first non-blank character is `#` are comments: a defs file is a
/// library, and a library wants a header. (Query text proper has no
/// comment syntax — queries are one-liners; the annotation lives in
/// the defs file.)
pub fn parse_defs(text: &str) -> Result<Defs> {
    let tokens = lexer::lex(&strip_defs_comments(text))?;
    parser::parse_defs(&tokens)
}

/// Blank out the `#` comment lines of a defs file, preserving line
/// structure. Session layers that *prepend defs text to query text*
/// (quai, the notebook kernels) must store the stripped form — the
/// query lexer itself has no comment syntax.
pub fn strip_defs_comments(text: &str) -> String {
    text.lines()
        .map(|l| {
            if l.trim_start().starts_with('#') {
                ""
            } else {
                l
            }
        })
        .collect::<Vec<_>>()
        .join("\n")
}

/// Like [`run`], with a pre-seeded fragment table (a `--defs` file);
/// inline `def` statements extend it.
pub fn run_with_defs(query: &str, defs: &Defs, adapter: &impl AstAdapter) -> Result<QueryResult> {
    let tokens = lexer::lex(query)?;
    let ast = parser::parse_with_data(&tokens, defs.clone(), Some(adapter))?;
    exec::gate_shell(&ast, adapter)?;
    checked(exec::eval(&ast, adapter))
}

/// Like [`run_traced`], with a pre-seeded fragment table.
pub fn run_traced_with_defs(
    query: &str,
    defs: &Defs,
    adapter: &impl AstAdapter,
) -> Result<Vec<(NodeId, Option<Value>)>> {
    let tokens = lexer::lex(query)?;
    let ast = parser::parse_with_data(&tokens, defs.clone(), Some(adapter))?;
    exec::gate_shell(&ast, adapter)?;
    checked(exec::eval_traced(&ast, adapter))
}

/// Like [`run_traced_prov`], with a pre-seeded fragment table.
pub fn run_traced_prov_with_defs(
    query: &str,
    defs: &Defs,
    adapter: &impl AstAdapter,
) -> Result<Vec<exec::Traced>> {
    let tokens = lexer::lex(query)?;
    let ast = parser::parse_with_data(&tokens, defs.clone(), Some(adapter))?;
    exec::gate_shell(&ast, adapter)?;
    checked(exec::eval_traced_prov(&ast, adapter))
}

/// True if `query` (inline defs allowed) opens with an expression
/// head or a lone root anchor and never navigates a document: every
/// branch is a step-less, projection-less root anchor and there are
/// no correlations. Such a query can run against a bare root with
/// no mounted target — the calculator invocation (spec: The
/// Expression Head). Unparseable text answers `false`; the caller's
/// normal run path owns the error message.
pub fn is_calculator(query: &str) -> bool {
    let Ok(tokens) = lexer::lex(query) else {
        return false;
    };
    let Ok(ast) = parser::parse_with_defs(&tokens, Defs::default()) else {
        return false;
    };
    ast.correlations.is_empty()
        && !ast.branches.is_empty()
        && ast.branches.iter().all(|b| {
            matches!(b.anchor, ast::Anchor::Root) && b.steps.is_empty() && b.projection.is_none()
        })
}

/// Parse `query` (expanding defs) and render it back to canonical
/// query text — the expansion lens (`qua --expand`). Pure fragments
/// only: a data-aware macro (`&name!`) needs [`expand_with`].
pub fn expand(query: &str, defs: &Defs) -> Result<String> {
    let tokens = lexer::lex(query)?;
    let ast = parser::parse_with_defs(&tokens, defs.clone())?;
    Ok(unparse::unparse(&ast))
}

/// The `macroexpand-1` lens (`qua --expand-1`): parse fully, but
/// return each directly-invoked macro's generated text before its
/// re-expansion — one entry per direct invocation, in source
/// order. Run the printed text again to take the next step;
/// [`expand`] is the fixed point.
pub fn expand_first(query: &str, defs: &Defs) -> Result<Vec<String>> {
    let tokens = lexer::lex(query)?;
    parser::parse_first_steps(&tokens, defs.clone(), None)
}

/// Like [`expand_first`], with the dataset at hand, so data-aware
/// macros (`&name!`) can read it at expansion time.
pub fn expand_first_with(
    query: &str,
    defs: &Defs,
    adapter: &impl AstAdapter,
) -> Result<Vec<String>> {
    let tokens = lexer::lex(query)?;
    parser::parse_first_steps(&tokens, defs.clone(), Some(adapter))
}

/// Like [`expand`], with the dataset at hand, so data-aware macros
/// (`&name!`) can read it at expansion time.
pub fn expand_with(query: &str, defs: &Defs, adapter: &impl AstAdapter) -> Result<String> {
    let tokens = lexer::lex(query)?;
    let ast = parser::parse_with_data(&tokens, defs.clone(), Some(adapter))?;
    Ok(unparse::unparse(&ast))
}