A Runtime Datalit Loader

A report, and a direction rather than a committed plan: a fast reader for datalit text that depends only on the runtime, usable as the compiler's reader for require data today and as the datalit loader of an I/O-capable sys package later. Written October 2026 after measuring the store demo; not scheduled.

Contents

What prompted this

The benchmark tables in Tuning the JIT and Are We Fast Yet for benchvs report the running phase of datalove script --time. The store demo's compiling phase is as large or larger, and almost all of it is reading its data files:

Orders .dlt data Compiling Running (jit)
20,000 4 MB 265-295 ms 245-255 ms
200,000 36 MB 2376 ms 1690 ms

That is about 15 MB/s. In a profile of the whole run, the front end's share is about a third: the salsa lexer (lex_chunk), the bracer, the datalit parser building AST nodes (ExprFull::new), interning (InternedText through the salsa table), the datalit typechecker (tycheck::check, check_int_fits_type), then datafile::Reader and parse_int_const. A text parser for a format this regular should run at hundreds of MB/s.

How a data file is read today

require data binds a .dlt file as a constant of a declared type. The file goes through the whole language front end:

  1. lexed, bracer-matched and parsed into a datalit AST, as source code is;
  2. names resolved;
  3. typechecked against the binding's type by the datalit typechecker;
  4. read into a ConstValue of that type by datafile::Reader (datafun-lower/src/datafile.rs), a walk led by the type, memoized by salsa on the source and the type.

Every step allocates a structure the size of the data -- the token tree, the AST, the interned strings, the ConstValue -- and the next step walks it.

How a constant gets back to the runtime

A ConstValue is not what a program runs on, so it is turned back into a runtime value wherever it is used:

The other direction exists too, for compile-time evaluation: extract_const_value (datafun-interp/src/ctfe.rs) runs a const's code in the interpreter and converts the result. It is a deep, IrType-led walk that builds a fresh owned tree: every list element a node, sets and maps walked leaf by leaf, strings copied into Rust Strings, bigint limbs into a Vec<u32>, and a struct as Struct(Vec<(String, ConstValue)>) -- the field names copied for every record. For 200,000 orders with nested lines that is millions of small allocations for names alone.

The loader

A reader for datalit text, in datalove-rt or a crate depending only on it and datalove-rtdt, producing runtime values directly:

Not through ConstValue

The obvious way to use the loader in the compiler -- load, then convert the runtime value to a ConstValue with extract_const_value, and let the rest of the pipeline carry on -- gives most of the gain back. A data file would go bytes, runtime value (fast), ConstValue tree (a deep copy, allocation-heavy, larger than the data), runtime value again (write_const, or emitted construction code): two extra full passes, and the tree is part of what costs today.

So a big data file should not become a ConstValue at all:

That needs a new kind of constant, roughly "loaded from these bytes against this type", which IrType's consts and the static-const machinery (Consts Built Once) carry beside ConstValue rather than expanding into one. That is the larger part of this work, and the part that makes it worth doing in the compiler rather than only for riders.

One reader, not two

The risk is two implementations of datalit typing: the salsa typechecker for literals in code, the loader for data, drifting on the questions literal typing answers -- whether an integer fits, how list elements unify, the empty collection defaults, struct field order. Two mitigations, best together:

What exists and what is missing

Exists. The runtime constructs every kind of value: lists, maps, sets, tensors and tables (*_create_local, push, insert, build_from_slice), strings (string_from_bytes), bigints (int_from_limbs, int_from_fixed), data (data_from_local). datalove-rt depends only on rmx, rtdt and the ABI declarations, so a loader in it is runtime-only by construction. The rti table is the export path. data is the untyped result.

Missing.

  1. A runtime descriptor table. Synthesis has to create descriptors. Today that is TyDescTable in datalove-datalit, keyed on salsa typechecker types, which cannot come along; it already has runtime-only caches for option, result, list, map and set, which is the shape needed. A small hash-consed table in the runtime, owning its allocations and keyed structurally.
  2. Decimal bigint parsing in the runtime, into limbs. Small.
  3. Type names in hints. : Order / names an alias. Checking mode barely needs one, the expected descriptor covering the value; synthesis mode needs a name environment from the caller, or refuses.
  4. The constant path in Not through ConstValue.
  5. Structured errors, and the compiler's rendering of them.

Phases

  1. The loader, checking mode, with the runtime descriptor table and bigint parsing, tested against the datalit typechecker on fixtures and generated literals. Usable from Rust and through the rti table; nothing in the compiler uses it yet.
  2. Synthesis mode, hints included, and an alias table supplied by the caller.
  3. The compiler checks data files with it, replacing steps 1-3 of today's path for diagnostics, while datafile::Reader still produces the ConstValue. Measures the parser's share alone.
  4. Loaded constants. The new kind of constant, the interpreter's static pool loading on first read, AOT embedding bytes and loading at startup, and datafile::Reader retired. This is where the store demo's compile time mostly goes.
  5. An I/O sys package exposing it, when there is one.

Each stands alone; 1 and 2 are useful to a rider before the compiler touches any of it.

Open questions