RDF parsing strategy in the Lean tree: layers, entry points, proofs, costs#

Status: record of the design as it stands on 2026-09-03, written because the owner asked for it to live somewhere findable (2026-09-03: "this and similar ought to be recorded properly somewhere I can find it. Ephemeral coding logs aren't that place"). Update this file when a layer, an entry point or a proof changes; the worknote index and the factoidal-lean-basics skill link here.

1. The shape: one meaning, several executions, proofs between them#

Every concrete syntax has ONE reference parser that is the meaning, ported from the F* source production by production. Faster or streaming executions are added beside it, never instead of it, and each one carries a theorem (or, until the theorem lands, a differential test) that it computes the same function as the reference on the inputs it accepts. The W3C syntax suites gate the reference parser; byte-identity of packed artifacts and the theorems gate the other executions.

2. Turtle#

2.1 Reference parser: L4Factoidal/Syntax/Turtle.lean#

2.2 Streaming execution for the shard packer#

Modules, in data order:

  1. Utf8Stream.lean: bounded incremental UTF-8 decoding of 64 KiB byte chunks; keeps at most the final three undecoded bytes; rejects invalid interior data; never chooses a statement boundary.

  2. TurtleStatementScan.lean: a per-character mode machine (normal, iri, comment, short string, long string, opening-quote run) that proposes candidate statement texts. A candidate ends at a . in normal mode followed by whitespace or #, or at a line end when the current text starts with PREFIX, BASE or VERSION (the SPARQL-style directives have no dot). It keeps head, the first seven characters after leading whitespace, so the directive test is O(1) per line end. The scanner never decides syntax; the grammar accepts or rejects every candidate.

    Two questions the owner asked on 2026-09-03, with the tests that answer them:

  3. TurtleChunkFold.lean: hands each candidate to readStatement from the reference grammar with the fuel text.length + 2 computed once per candidate, carrying TurtleState (prefixes, base, blank-node prefix, mode) across candidates and chunks; retains only the unfinished candidate.

  4. Harness/PredicateShardPack.lean: a first pass over the file computes the SHA-256 and the blank-node prefix; the second pass feeds chunks to the fold and publishes the completed triples as IBK3 blocks every 64 chunks (about 4 MiB), so a predicate whose rows span several windows gets several blocks (the 309 blocks over 232 predicates of the UK Parliament dump).

The scanner is used only by the shard packer; the WASM mirrors do not contain it.

2.3 Proofs#

2.4 Costs paid and costs open#

Date Cost Cause Fix
2026-04 stack overflow on large Turtle non-tail recursion docs/2026-04-21-large-turtle-stack-overflow-fix-sketch.md
2026-09-02 quadratic parseTurtle per-token cs.length + 1 fuel in two literal readers constant literalFuel + proofs
2026-09-02/03 UK Parliament pack 6,134 s scanner reversed the whole candidate at every line end; a 134 MB, 4,211-line statement group seven-character head + proof; pack 254 s
open 22.9 s user + 21.7 s system and 5.39 GB resident to parse 134 MB (measured 2026-09-03) List Char representation: about 16 bytes and one allocation per character, built twice (scanner currentRev, then text.toList for the grammar) a String/ByteArray-position lexer with its own equality proof against 2.1
2026-09-04 named-graph IBK4 pack quadratic; 553 MB over 194 graphs killed by the operating system after 1 h 57 min NQuadsFast.addQuadFast read the graph with getElem? and inserted it back, so Std.HashMap.insert copied the whole bucket map of that graph per quad Std.HashMap.modify, which consumes the map; proofs restated through getElem?_modify. 104 MB over 50 graphs: 268.73 s to 103.12 s (2026-09-04-ibk4-named-graph-packing-scale.md)
2026-09-04 IBK4 pack held a whole-file String.toList quadArtifacts took the source as one String PackStream.quadIngestFeed streams the N-Quads grammar in 65,536-byte chunks; byte identity by NQuadsFold.streamConsume11_eq_batch. 104 MB over 50 graphs: peak memory 2,531,999,744 to 1,127,907,328 bytes
2026-09-05 the same whole-file String.toList for a TURTLE source into IBK4 nothing called TurtleChunkFold; the quad route needs a Dataset and the fold hands back statements PackStream.QuadStream carries the Turtle chunk fold beside the N-Quads stream, quadStreamDataset closes either. 104,179,872-byte Turtle source: peak resident set 2,788,786,176 to 1,253,408,768 bytes (26.8 to 12.0 bytes per source byte), generation byte-identical. Byte identity MEASURED, not proved — see 2.3
open peak IBK4 memory is still about ten times the source one block per predicate over the whole source, so every row and every encoded block is live at the manifest several blocks per predicate, which changes the emitted block set and needs a wire-version decision (2026-09-04-ibk4-named-graph-packing-scale.md)
open multi-megabyte literals cost seconds each in the packer every literal goes through the term codec, PTD1 pages, TLI1 keys and Merkle leaves large-literal policy (corpus ladder, docs/20260902-persisted-query-ladder.md)

3. The other syntaxes, briefly#

Syntax Reference Other executions Agreement
N-Triples NTriples.lean parseNTriples — W3C suite; NTriplesRoundTrip.lean
N-Quads NQuads.lean parseNQuads NQuadsFast.lean parseNQuadsFast (bucketed accumulator; the WASM datasetOpen path); NQuadsStreaming.lean (chunk-boundary fold), NQuadsFold.lean (the generic consumer the IBK4 shard packer streams through) parseNQuadsFast_eq_parseNQuads proved 2026-09-02 (NQuadsFastTheorems.lean); the streaming module carries its own chunk-boundary theorem
TriG TriG.lean parseTriG (shares the Turtle productions; the default graph is the unlabelled block) none; an IBK4 pack over TriG still buffers the whole source, unlike the Turtle and N-Quads routes (quad-aware layout: 2026-09-02-quad-aware-block-layout.md) W3C suites
RDF/XML RdfXml.lean — W3C suite; RdfXmlTheorems.lean (blank-node label spaces disjoint by construction)

Shared lexical pieces: Lexing.lean (the N-Triples string body and escape table the Turtle decodeEscape mirrors), IriResolve.lean (RFC 3986), IriScan.lean, the Locality*.lean family (chunk-locality lemmas used by the N-Quads streaming proofs).

4. Rules that follow#

  1. A new fast path is added beside the reference parser with a theorem or, until the theorem lands, a differential tool named in this file.
  2. Any per-character or per-line decision in a streaming scanner reads O(1) state, never the accumulated text (skills/lean4-performance).
  3. Fuel is computed once per document or per candidate, never per token (skills/lean4-proof-patterns section 2 has the theorem shape).
  4. Before reading code for a superlinear ingest, run a size ladder without the suspect inputs and isolate the region that misbehaves (docs/20260902-persisted-query-ladder.md, "Where the 6,134 s went").