Graven

zidl Runtime + Benchmarks (Phases 1 and 2) Implementation Plan

Historical document. Written on 2026-11-12, when the project was called zidl (later Graven). It is the plan that built the first runtime and the benchmarks against FlatBuffers and Cap'n Proto. Paths, names and some APIs changed afterwards; the normative text is the format specification.

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: A Rust runtime for the zidl format (zero-copy reads, forward writing with a pool of BytesMut), hand-written "generated" code for 4 scenarios, evolution tests, and benchmarks against FlatBuffers and Cap'n Proto.

Architecture: A Cargo workspace with zidl (runtime), zidl-layout (the layout algorithm, reused by the compiler in phase 3), zidl-schemas (the code the compiler would generate, written by hand) and zidl-bench (criterion + flatbuffers + capnp). The format is validated by benchmark before investing in the compiler.

Tech Stack: Rust 2024 (rustc 1.94), bytes, proptest, criterion, flatbuffers, capnp/capnpc. External tools: capnp 1.5 (installed), flatc (not installed — sudo pacman -S flatbuffers; the generated code is committed, so only whoever regenerates it needs it).

Spec: design/2026-11-12-zerocopy-idl-design.md

Global Constraints

Review Focus

  1. Random/truncated bytes / an offset pointing outside or before 0 / len = u32::MAX → Err, never a panic (proptest in Task 5; fixed cases in Task 2).
  2. An abandoned streaming list (drop without close, or ? exiting halfway) and a Builder dropped without finish → no panic, buffers return to the pool (Tasks 3 and 4).
  3. Bytes of a previous message still alive while the same pool builds the next one → the old message stays intact (Task 3).
  4. An empty string present vs absent; an empty list present vs absent → distinguishable (Task 3).
  5. A streaming list element with a string + a nested table + a nested streaming list → correct offsets after the fix-up (Task 4).

File Structure

Cargo.toml                         # [workspace] members = ["crates/*"]; remove src/ and the root [package]
crates/zidl/Cargo.toml
crates/zidl/src/lib.rs             # #![no_std], re-exports
crates/zidl/src/error.rs           # Error
crates/zidl/src/le.rs              # trait Le
crates/zidl/src/read.rs            # TableRef, Bit, read lists
crates/zidl/src/pool.rs            # BufPool, SimplePool
crates/zidl/src/write.rs           # Builder, TableW, TableListW, StreamListW, ScalarListW
crates/zidl/src/verify.rs          # VerifyCtx
crates/zidl/tests/fuzz_props.rs    # proptest: arbitrary bytes
crates/zidl-layout/src/lib.rs      # FieldKind, compute_layout
crates/zidl-schemas/schemas/*.zidl # bank, game, iot, stream (source for phase 3)
crates/zidl-schemas/src/{bank,game,iot,stream}.rs
crates/zidl-schemas/src/evo/{bank_v1,bank_v2,bank_v3}.rs
crates/zidl-schemas/tests/{layout_consts,roundtrip,evolution}.rs
crates/zidl-bench/schemas/{bank,game,iot,stream}.{fbs,capnp}
crates/zidl-bench/src/fbs_gen/*.rs # generated by flatc, committed
crates/zidl-bench/build.rs         # capnpc
crates/zidl-bench/src/{lib,data,zidl_impl,fbs_impl,capnp_impl}.rs
crates/zidl-bench/src/bin/{sizes,allocs}.rs
crates/zidl-bench/benches/codec.rs
docs/benchmarks/2026-11-12-baseline.md

Task 1: Workspace, Le, Error, reading scalars and presence

Files:

Interfaces:

// table: fixed_len=7: [bits][u32 a @1][u16 b @5]; presence a=bit0, b=bit1, bool=bits2/3
fn sample() -> Vec<u8> { vec![7,0, 0b0000_1101, 42,0,0,0, 0,0] } // a present=42, b absent, bool present=true
#[test] fn reads_present_scalar() { let t = TableRef::at(&sample(),0).unwrap();
  assert_eq!(t.scalar::<u32>(1, Bit{byte:0,shift:0}).unwrap(), Some(42)); }
#[test] fn absent_bit_is_none() { /* b at off 5, Bit{0,1} */ assert_eq!(.., None); }
#[test] fn bool_two_bits() { assert_eq!(t.bool(Bit{byte:0,shift:2}), Some(true)); }
#[test] fn slot_beyond_fixed_len_is_none() { /* off 7 size 4 */ assert_eq!(t.scalar::<u32>(7, Bit{byte:0,shift:4}).unwrap(), None); }
#[test] fn bit_byte_beyond_fixed_len_is_none() { /* Bit{byte:9,..} */ }
#[test] fn partial_slot_is_truncated() { /* off 5 size 4 */ assert_eq!(t.slot_state(5,4), Err(Error::Truncated)); }
#[test] fn header_past_end_is_err() { assert_eq!(TableRef::at(&[9,0,1],0).err(), Some(Error::OutOfBounds)); }
#[test] fn le_roundtrip_all_types() { /* write_le→read_le for each type, incl. f64::NAN bits, i128::MIN */ }

Task 2: Reading variable-size data

Files: Modify crates/zidl/src/read.rs

Interfaces:


Task 3: Pool, Builder and TableW (without table lists)

Files: Create crates/zidl/src/pool.rs, crates/zidl/src/write.rs

Interfaces:


Task 4: Table lists (known count and streaming) with fix-up

Files: Modify crates/zidl/src/write.rs

Interfaces:


Task 5: VerifyCtx and properties with arbitrary bytes

Files: Create crates/zidl/src/verify.rs, crates/zidl/tests/fuzz_props.rs

Interfaces:


Task 6: zidl-layout — the layout algorithm

Files: Create crates/zidl-layout/{Cargo.toml,src/lib.rs}

Interfaces:

cursor = 0; open = None  // (byte_pos, next_shift)
for f in fields:
  need = match f { Scalar => 1, Bool => 2, Ptr | Union => 0 }
  if need > 0:
    if open is Some((p, s)) and s + need <= 8: presence = (p, s); open = (p, s+need)
    else: presence = (cursor, 0); open = (cursor, need); cursor += 1
  if f is not Bool: slot = cursor; cursor += size(f)
fixed_len = cursor

Task 7: zidl-schemas — hand-written generated code for the 4 scenarios

Files:

Schemas (syntax of spec §6; IDs in order):

// bank.zidl
enum Currency : u16 { BRL = 0, USD = 1, EUR = 2 }
table Entry { account @0: u64; amount @1: i64; memo @2: string?; }
table Transaction { id @0: u64; ts @1: u64; currency @2: Currency; from_shard @3: u32;
                    to_shard @4: u32; entries @5: [Entry]; note @6: string?; }
// game.zidl
struct Vec3 { x: f32, y: f32, z: f32 }
table Entity { id @0: u32; pos @1: Vec3; vel @2: Vec3; hp @3: u16; alive @4: bool; }
table Frame { tick @0: u64; entities @1: [Entity]; }
// iot.zidl
table Telemetry { device @0: u32; ts @1: u64; temp @2: f32; humidity @3: f32?; battery @4: u8; }
// stream.zidl
table Chunk { stream_id @0: u64; seq @1: u32; data @2: bytes; }

Interfaces (the pattern the phase 3 compiler will reproduce — fixed here):


Task 8: Evolution matrix

Files: Create crates/zidl-schemas/src/evo/{mod,bank_v1,bank_v2,bank_v3}.rs, crates/zidl-schemas/tests/evolution.rs

Versions (only Transaction changes; Entry is the same):


Task 9: Equivalent implementations in FlatBuffers and Cap'n Proto

Files: Create crates/zidl-bench/{Cargo.toml,build.rs}, schemas/*.fbs, schemas/*.capnp, src/fbs_gen/*.rs, src/{lib,data,zidl_impl,fbs_impl,capnp_impl}.rs

Interfaces:


Task 10: Benchmarks, sizes, allocations and report

Files: Create crates/zidl-bench/benches/codec.rs, src/bin/sizes.rs, src/bin/allocs.rs, docs/benchmarks/2026-11-12-baseline.md

Interfaces: Consumes everything from Task 9.