Superseded. Replaced by the sequence and the span table.

The design between the relocatable heap and the present one: the file’s content as allocations that pointed at each other, described on copy-on-write pages by an address table of statements keyed by an allocation’s id and an offset in it. Implemented in full by kladde-rs, whose branch address-table holds it as it stood, measured in most of the evaluation pages, and specified in detail; kladde-docs’ history holds every document of it, up to commit bf13c73. What follows is its outline, and why it was replaced.

What it was

Allocations. An allocation was an untyped byte range with a stable 32-bit id, a size, and content; ids were never zero, which left zero for a null pointer. A value lived in an inline slot of a fixed INLINE_SIZE inside some allocation, and a value with content of variable length — a string, a vector, a map — owned allocations of its own, reached through a pointer serialized as the target’s id. Every allocation had exactly one owning pointer, so the value graph was a tree of ownership; dropping or overwriting an owning value freed what it owned, recursively, in the same transaction and after the write that superseded the pointer.

The address table was a list of statements in table pages, each inheriting its page’s epoch: Ref(id, offset, size, address), Zero(id, offset, size), and Inline(id, offset, size, payload) stated content, each with a sizing variant that also said the allocation ended where the content ended; Size(id, n) stated a size alone, and Tombstone(id) ended an allocation’s existence. Statements of different epochs were resolved per byte, the newest winning wherever it applied, and an allocation’s size was its newest size statement’s. A writer had to keep the coverage rule — every byte that became part of an allocation was stated in that epoch — or truncated content could reappear.

The journal recorded effects on allocations: Free, Resize, Write, Splice, Copy and Move, each but Free bringing its allocation into existence. Every mutation decomposed as prepare new, unreachable state → one publishing write → clean up what it replaced, an ordering discipline no type system enforced.

In memory, a loaded file was a fragment map from (id, offset) to the resolved content, a statement slab counting each live statement’s pins — the fragments, sizes and tombstones that kept it alive — an allocation map of sizes, fragment counts and mentions, and the page table. A flush folded the journal into a piece table per allocation, took the fragments it changed, and derived every statement from one dirty set at the cut; consolidation added a fourth mechanism, description defragmentation, which rewrote stippled ranges of an allocation as one statement, materializing the zeros between them.

Values came in slotted and packed places, small values kept short content inline, and the Rust binding had Location, UniquePointer, store, load and free, with one hole through which load could mint a second owner.

Why it was replaced

The description was paid per allocation and a splice per allocation’s tail, so keeping either small made the other grow. A small description wanted values inline in few allocations, and then each allocation was long, and a splice into one restated or rewrote everything behind it, since statements were keyed by their offset in the allocation: on kladde-svg’s small layout of the world map, flushes were measured to restate 24 bytes of statements per edit that had lain in leaves and to write 16 bytes of data again, growing with the length of the lists. Cheap splices wanted short allocations, and then each value paid its pointer and statements, a cost that grew with the number of values rather than with their bytes. No policy escaped that, because the format tied the unit of description to the unit of the data model. The one-sequence design untied them: the description depends only on how a writer cuts one sequence into spans, and spans are keyed by id, so a splice restates the spans at its cuts and nothing behind them. The file as one sequence, measured compares the two on the same drawings and texts.

Compared with the present design

allocations and the address tableone sequence
description written per spliceevery statement behind it in its allocation, or its bytes written againone to three entries
moving a value within a containerthe moved bytes, or Move’s statement editsthree to six spans, no bytes
moving a value between containersits pointers written again, its allocations left where they area cut and a paste in one transaction: three to six spans, no bytes
description written per overwriteone statementthree entries the first time, one after
description per valuea pointer, its statements, and its sizenone; a few bytes per span of up to a page
what a page rewrite must also statethe sizes and tombstones of the ids it touchesnothing
livenesspins over fragments, size pins, tombstones, mentionsthe newest entry of a reached span
the store’s memoryfragment map, statement slab, allocation map, page tablespan table, rope, page table, reverse index
removing a valuea splice, then a free walk over what it ownsa splice
field access when mutatingconstant offsetsconstant up to the first variable-size field
openinga sweep resolving bytes across epochsone pass over entries, a chain walk, and the decode
upgrading to a new schemathe allocations holding values whose layout changed, written againthe values whose encoding changed, spliced in again
writing in the writer’s schemaoffsets from the writer’s inline sizesoffsets and encodings interpreted from the writer’s descriptors
parts of a file in different schema versionsnot supported; a capsule would take an allocationnot supported; a capsule would be inline
toolingopaque containers, a pointer grapha decode from the schema

What the present design pays for it: containers keep positions in memory, four bytes per variable-size element; offsets after a value whose size depends on its value are computed at run time; making a guard costs a lookup in its parent’s index and a change of size an update per level; an overwrite inside a long span splits it; every insertion into a sequence takes two records, the element and the count; moving a value between containers holds a transaction open; and partial loading needs segments, which need a pointer of their own.

The vocabulary that went with it

termwhy it wentinstead
allocation, allocation id, allocation sizethere are nonethe sequence, and a value’s region
pointer, owning pointer, ownership, UniquePointer, Locationnothing points at anythinga region; a segment’s reference, later
free, freeing, free walk, leaknothing is allocatedremoving, deleting: a splice
tombstone, size statement, sizing variant, Sizeno size is stated—
coverage ruleno byte becomes part of an allocationthe reach rule
fragment, fragment map, stipple, shadowing of part of a statementno entry shows part of a spanspan, run of spans; an entry replaces another whole
pin, mentions, statement slab, allocation map, recyclable setliveness is one bitlive entry, reached span
Resize, Free, Copynothing to act on—
slotted and packed places; inline size, INLINE_SIZEa type has one encodingfixed-size, variable-size, Fixed(T), encoded size
small value, spillingeverything is inline—
root allocation, schema table, consolidator state, as allocationsthey are chainsthe sequence, the schema chain, the private chain
address table, statemententries describe spans wholespan table, entry
description defragmentationthere is no stipple to fillmerging spans