Data runtime in 0.1.0
Canonical Quirl project documentation synced from docs/data-runtime.md.
Streaming failure model and invariants
- Cancellation is checked before source initialization, before and during every pull, and before each transform or renderer write. Cancellation after a partial write remains an error; already-written bytes are not reported as a successful value.
- CLI execution also arms the plan's one monotonic absolute deadline before data initialization. Expiry sets the same engine-observed cancellation flag, covers parsing, adapters, transforms, pulls, materialization, and interactive writes, and is normalized back to the plan deadline error only after readers and external processes have cleaned up. The external-process policy may choose its earlier two-second security deadline but cannot extend the plan.
- A lazy source owns its reader until the stream is dropped. Adapter failure after earlier rows does not manufacture a successful partial result, and dropping the stream after success, cancellation, or error closes the reader.
- Values remain
DataValuefrom source ingress through transforms and shared execution outcomes. JSON, YAML, TOML, and byte strings are named conversion boundaries; none is the evaluator's internal representation. - Every conversion validates bytes, rows, record width, nesting depth, value
nodes, retained text, and materialized bytes. A bridge fails with
ResourceLimitbefore retaining the first value beyond its configured bound, and reports the configured limit and observed use when safe. sort, table rendering,DataOutput::into_envelope, and the collected convenience APIs are the only materializing stream operations. Lazylines, CSV, tar,where,get,select, andtakeretain an O(one-row) window.- Type mismatches and lossy conversions are operating errors. No adapter, transform, or renderer panics on user input, recurses over untrusted value structure, or performs a hidden JSON round trip.
firstis the focused grammar's optional operation: an empty stream becomesOption::Noneand a present row becomesOption::Some. Operating failures remainShellErrorunless a caller explicitly requests a result envelope.Taskremains bounded declarative state; this runtime does not claim a scheduler or asynchronous execution.- A successful shared execution outcome has one zero status and one typed value
representation. Cancellation, adapter failure, bridge overflow, and
transform errors remain
Err(ShellError)after owned readers are released; they never become a successful status with a partial value.
quirl data evaluates native structured values and renders them explicitly:
quirl data 'open users.csv | where enabled == true | select name' --format table
quirl data 'open config.toml | get service.port' --format plain
quirl data 'open users.json' --format json
quirl data '^external printf "{\"ok\":true}" | from json'Syntax ownership and analysis
quirl-data owns the focused data lexer, AST, inert diagnostics, formatter,
and data-token highlighting because those contracts name data sources,
byte/value bridges, transforms, predicates, and data-domain types.
quirl-syntax remains the independent command-grammar foundation. This keeps
the accepted ADR 0016 dependency graph unchanged: the CLI composition root
maps data diagnostics into ShellError for execution and check/format
boundaries, and injects a side-effect-free native analyzer callback into
quirl-lsp rather than adding an LSP-to-data dependency.
Parsing never opens a path, reads the current directory, invokes an adapter, or
calls an external process. The iterative lexer and literal parser preserve
half-open UTF-8 byte spans and reject input before retaining more than 256 KiB
of source, 32,768 tokens, 64 levels of nesting, 100,000 AST nodes, 256 fields
per record/field list, or 64 KiB per decoded literal by default. Library
callers can replace every syntax budget through DataSyntaxLimits.
The AST describes the currently implemented pwd, files/ls, open, JSON
literal, and ^external sources; lines, from json, and to json bridges;
and get, where, select, sort, take, first, and length transforms.
Its DataType surface names existing scalar/domain DataValue forms plus
List, Record, Table, Option, Result, Task, Stream, and Command.
Those type nodes remain focused semantic vocabulary rather than general type
inference. The evaluator now preserves DataValue through live streams and
implements first as Option; Task still does not claim scheduling.
An empty expression is now a syntax diagnostic instead of the earlier
implementation accident that produced null; write null explicitly. Bare
paths containing whitespace also receive a migration diagnostic directing the
author to quote the complete path.
--format json emits a tagged Value or Stream envelope so a script
does not need to infer whether an array is an ordinary value or a pipeline
stream. Values themselves retain an ABI tag (int, decimal, path, size,
and so on) rather than making domain values look like strings. The current
native parser emits generic scalar/list/record values. Filesystem rows preserve
Path, Size, and optional DateTime fields; tar rows preserve Path and
Size; TOML datetimes remain DateTime. Option, Result (ok or error),
and Task (pending, complete, cancelled, or failed) remain explicit in
the same ABI. first produces Option::Some or Option::None.
DataRuntime::eval_result_envelope is the intentional boundary that captures
an operating error; other evaluator entry points return ShellError. Task
states are validated declarative data and never start asynchronous work.
Supported adapters are JSON, YAML, TOML, CSV, uncompressed POSIX tar archive
inspection, and filesystem rows (files [path], with ls retained as an
alias). CSV and tar entries are pull-based: a row is parsed when the consumer
asks for it, and cancellation is checked before each pull. The public CLI
writes plain and JSON rows directly to stdout as it pulls them, keeping those
paths O(window) rather than constructing a complete output string. It keeps
that laziness through where, get, select, and take; sort, table
rendering, DataOutput::into_envelope, and the collected eval/render
convenience APIs are deliberate bounded collection boundaries. render_to is
the streaming output boundary. eval_typed is the collected typed API;
the older eval method is retained as a named JSON-compatibility bridge for
script and watch consumers and therefore applies the same documented loss as
to json.
JSON, YAML, TOML, and directory entries are validated then materialized because
their current underlying parsers expose whole-document APIs.
Every adapter enforces the default 8 MiB file size, 100,000 row/node, 256
field, 64 nesting-depth, 8 MiB retained-text, 16 MiB materialization, and
256 KiB expression limits. Library callers can set DataLimits explicitly;
evaluator entry points derive matching syntax byte, depth, and field limits
before parsing. Resource-limit diagnostics report the configured limit and
observed use when safe. CSV requires a single unique header row
and does not support multiline quoted fields. Tar inspection lists headers
only: it never extracts entries and intentionally supports only uncompressed POSIX .tar
archives (not zip, gzip, bzip2, xz, or PAX/GNU extended-name semantics); each
header checksum is verified before the entry is reported.
Byte/value crossings are explicit. lines turns one string byte value into a
lazy stream of newline-delimited strings; from json parses a string byte
value (or each string stream item); and to json serializes a value or each
stream item. JSON never creates domain tags. At to json, paths, datetimes,
and patterns become strings; sizes and durations become strings suffixed with
B and ns; non-finite decimal text is rejected rather than silently changed.
YAML requires string mapping keys and rejects application-specific tags. TOML
converts directly without passing through JSON. ^external <command> is the
only external byte producer. A
standalone DataRuntime has no ambient process capability and rejects it. The
CLI injects the sandboxed process host only when the plan declares process
authority, with a two-second engine-local deadline and a 1 MiB combined
retained-output limit. The local deadline is intersected with the plan through
the same shareable cancellation token. Non-zero exits remain ShellError failures with
the bounded stderr context, rather than being silently converted into values.
This release intentionally does not implement SQLite, zip/compressed archive
inspection, HTTP, background task scheduling, or a
fully streaming JSON/YAML/TOML parser. HTTP is not implied by open: a future
HTTP adapter must expose explicit request, timeout, redirect, byte, and
capability limits. Those remain design targets rather than silently available
0.1.0 behavior.
The rich interactive data branch also uses a shared ExecutionRequest with an
inherited output target. It writes terminal-safe plain values one row at a time,
flushes each row, checks the shared SIGINT/SIGTERM cancellation identity before
every pull and write, observes the same absolute plan deadline, and yields after
at most 16 pulls. A failed, cancelled, or expired
stream may already have produced scrollback but never commits its partial rows
to the picker cache. Only successful typed rows enter the 128-item, 512 KiB
session cache used by Alt-D; opening that picker performs no evaluation.
Nested Option wrappers are flattened iteratively and rejected before output
at a depth above 64, so renderer stack use is independent of source nesting.