Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"mcpServers": {
"too-many-cooks": {
"url": "http://localhost:4040/mcp"
}
}
}
21 changes: 21 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions Claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@ Read this file in full. Rules below are NON-NEGOTIABLE — violations are reject
⚠️ **TOKEN ECONOMICS DISCIPLINE.** Check file size first. `Grep` over `Read`. Use `offset`/`limit`.
Smallest diff that solves the problem. Delete dead code, unused imports, stale comments.
Call out irrelevant context before proceeding. Bloat degrades reasoning. ⚠️
⚠️ DON'T ASK THE USER QUESTIONS!!!

- DO YOUR JOB and don't say things like "Open questions before I start"
- USE YOUR JUDGEMENT! ⚠️
⚠️ NEVER KILL ANY VSCODE PROCESS ⚠️
⚠️ ACT AUTONOMOUSLY. DON'T ASK THE USER QUESTIONS. USE YOUR JUDGEMENT. ⚠️
⚠️ NEVER KILL ANY VSCODE PROCESS ⚠️

## Project Overview

Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

typeDiagram is a tiny, language-neutral DSL for describing **algebraic data types** — records, tagged unions, generics, aliases. From one `.td` file, you get:

- **Source code** in TypeScript, Python, Rust, Go, C#, F#, Dart, PHP, and Protobuf — DTOs, data classes, discriminated unions, pattern-matchable enums — generated from the same definition, always in sync.
- **Source code** in Typeshed, TypeScript, Python, Rust, Go, C#, F#, Dart, PHP, and Protobuf — DTOs, data classes, free functions, discriminated unions, and enums — converted through one model.
- **SVG diagrams** with automatic orthogonal layout — no dragging, no fiddling, versionable in git.
- **Round-trip conversion** from existing TypeScript/Python/Rust/Go/C#/F#/Dart/PHP/Protobuf back to the DSL, so you can retrofit an existing codebase.
- **Round-trip conversion** from existing Typeshed/TypeScript/Python/Rust/Go/C#/F#/Dart/PHP/Protobuf back to the DSL, so you can retrofit an existing codebase.

This is not a diagramming tool dressed up with a text input like Mermaid or PlantUML. typeDiagram is a **shared schema for your data model** — the diagram is a side effect, not the goal. The primary output is code, in as many languages as you need, kept strictly in sync by construction.

Expand Down Expand Up @@ -79,6 +79,8 @@ Three constructs: `type` (records), `union` (tagged sum types), `alias` (newtype
```sh
typediagram schema.td > diagram.svg # DSL → SVG
typediagram --from typescript types.ts > diagram.svg # TS → SVG
typediagram --from typeshed --emit td module.pyi # .pyi → typeDiagram
typediagram-typeshed /path/to/typeshed generated/ # complete checkout
typediagram --to rust schema.td > types.rs # DSL → Rust
```

Expand Down
8 changes: 4 additions & 4 deletions coverage-thresholds.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
"default_threshold": 90,
"projects": {
"packages/typediagram": {
"statements": 97.05,
"statements": 97.22,
"branches": 91.05,
"functions": 98.8,
"lines": 96.97
"lines": 97.18
},
"packages/cli": {
"statements": 99,
Expand All @@ -18,12 +18,12 @@
"packages/web": {
"statements": 97.7878787878788,
"branches": 96.46835443037975,
"functions": 98.05660377358491,
"functions": 99,
"lines": 97.7012987012987
},
"packages/vscode": {
"statements": 99,
"branches": 98.41,
"branches": 99,
"functions": 99,
"lines": 99
}
Expand Down
11 changes: 8 additions & 3 deletions crates/tdbin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@ publish = false
[lints]
workspace = true

# prost is the COMPETITOR baseline for the honest size/speed benchmark, pulled in
# only for the `size_gate` test and the `bench` example. It is a DEV-dependency so
# the shipped runtime stays zero-dep ([TDBIN-BENCH-CORPUS], [TDBIN-BENCH-GATE]).
# prost and rmp-serde are the COMPETITOR baselines for the honest size/speed
# benchmark, pulled in only for the `size_gate` test and the `bench` example.
# They are DEV-dependencies so the shipped runtime stays zero-dep
# ([TDBIN-BENCH-CORPUS], [TDBIN-BENCH-GATE]). The prost mirror structs carry
# `serde` derives so the SAME 1:1 fixture values feed the MessagePack baseline
# (`rmp-serde`) with no third struct set.
[dev-dependencies]
criterion = { version = "0.8.2", default-features = false, features = ["cargo_bench_support"] }
prost = "0.14"
serde = { version = "1", features = ["derive"] }
rmp-serde = "1"

# The TDBIN-vs-Protobuf speed benchmark, run via `cargo run --release --example bench`.
[[example]]
Expand Down
31 changes: 28 additions & 3 deletions crates/tdbin/benches/gate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use std::time::Duration;
use bench_corpus::{batches, corpus, documents, events};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use prost::Message;
use serde::de::DeserializeOwned;
use serde::Serialize;
use tdbin::{Struct, TdBin};

/// Return a TDBIN encoded message or terminate the benchmark process.
Expand Down Expand Up @@ -55,22 +57,35 @@ fn pb_bytes<P: Message>(value: &P) -> Vec<u8> {
}
}

/// Return a self-describing `MessagePack` (struct-as-map) message, or terminate.
fn mp_bytes<P: Serialize>(value: &P) -> Vec<u8> {
match rmp_serde::to_vec_named(value) {
Ok(bytes) => bytes,
Err(error) => {
eprintln!("msgpack encode failed before benchmark: {error:?}");
std::process::exit(1);
}
}
}

/// Benchmark all encode/decode operations for one paired corpus fixture.
fn bench_fixture<T, P>(c: &mut Criterion, label: &str, td: &T, pb: &P)
where
T: Struct + TdBin,
P: Message + Default,
P: Message + Default + Serialize + DeserializeOwned,
{
let bare = td_bytes(td);
let framed = td_framed_bytes(td, false);
let packed_framed = td_framed_bytes(td, true);
let protobuf = pb_bytes(pb);
let msgpack = mp_bytes(pb);
println!(
"[{label}] sizes: tdbin_bare={} tdbin_framed={} tdbin_packed_framed={} protobuf={}",
"[{label}] sizes: tdbin_bare={} tdbin_framed={} tdbin_packed_framed={} protobuf={} msgpack={}",
bare.len(),
framed.len(),
packed_framed.len(),
protobuf.len()
protobuf.len(),
msgpack.len()
);

let mut group = c.benchmark_group(format!("tdbin_vs_protobuf/{label}"));
Expand Down Expand Up @@ -105,6 +120,9 @@ where
});
},
);
let _ = group.bench_with_input(BenchmarkId::new("msgpack_encode", label), pb, |b, value| {
b.iter(|| rmp_serde::to_vec_named(black_box(value)));
});
let _ = group.bench_with_input(
BenchmarkId::new("tdbin_decode_bare", label),
&bare,
Expand Down Expand Up @@ -133,6 +151,13 @@ where
b.iter(|| P::decode(black_box(bytes.as_slice())));
},
);
let _ = group.bench_with_input(
BenchmarkId::new("msgpack_decode", label),
&msgpack,
|b, bytes| {
b.iter(|| rmp_serde::from_slice::<P>(black_box(bytes.as_slice())));
},
);
group.finish();
}

Expand Down
8 changes: 5 additions & 3 deletions crates/tdbin/examples/bench_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,19 @@ fn fixture<T, P>(
) -> Result<String, BoxError>
where
T: Struct + TdBin,
P: Message,
P: Message + serde::Serialize,
{
let bare = td.to_bytes()?;
let framed = td.to_framed_bytes(None)?;
let packed = td.to_packed_framed_bytes(None)?;
let msgpack = rmp_serde::to_vec_named(pb)?;
Ok(format!(
"{{\"name\":\"{name}\",\"shape\":\"{shape}\",\"corpus\":{corpus},\"logical_items\":{items},\"tdbin_bare\":{},\"tdbin_framed\":{},\"tdbin_packed_framed\":{},\"protobuf\":{}}}",
"{{\"name\":\"{name}\",\"shape\":\"{shape}\",\"corpus\":{corpus},\"logical_items\":{items},\"tdbin_bare\":{},\"tdbin_framed\":{},\"tdbin_packed_framed\":{},\"protobuf\":{},\"msgpack\":{}}}",
bare.len(),
framed.len(),
packed.len(),
pb.encoded_len()
pb.encoded_len(),
msgpack.len()
))
}

Expand Down
100 changes: 77 additions & 23 deletions crates/tdbin/src/intblock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ pub(crate) fn encode(values: &[i64]) -> Result<Vec<u8>, EncodeError> {
out.extend_from_slice(&first.to_le_bytes());
out.extend_from_slice(&floor.to_le_bytes());
out.push(u8::try_from(width).map_err(|_| EncodeError::LimitExceeded)?);
pack_bits(&deltas, floor, width, &mut out);
pack_deltas(&deltas, floor, width, &mut out);
Ok(out)
}

Expand Down Expand Up @@ -91,18 +91,44 @@ pub(crate) fn decode(block: &[u8]) -> Result<Vec<i64>, DecodeError> {
return Err(DecodeError::MalformedColumn);
}
let mut out = Vec::with_capacity(count);
let mut acc = i64::from_le_bytes(first.to_le_bytes());
let acc = i64::from_le_bytes(first.to_le_bytes());
out.push(acc);
match width {
0 => fill_constant(&mut out, acc, unzigzag(floor), count),
_ => read_deltas(&mut out, block, acc, floor, width, count),
}
Ok(out)
}

/// Accumulate the remaining values from a constant `step` delta: a width of
/// zero packs no bits at all, so monotonic columns skip the bit reader.
fn fill_constant(out: &mut Vec<i64>, mut acc: i64, step: i64, count: usize) {
for _ in 1..count {
acc = acc.wrapping_add(step);
out.push(acc);
}
}

/// Accumulate the remaining values from the packed delta stream.
fn read_deltas(
out: &mut Vec<i64>,
block: &[u8],
mut acc: i64,
floor: u64,
width: u32,
count: usize,
) {
let mut bits = BitReader::new(block.get(HEADER_BYTES..).unwrap_or_default());
for _ in 1..count {
let delta = unzigzag(bits.read(width).wrapping_add(floor));
acc = acc.wrapping_add(delta);
out.push(acc);
}
Ok(out)
}

/// Zigzagged wrapping deltas between consecutive values.
/// Zigzagged wrapping deltas between consecutive values, materialized once:
/// the floor scan, width scan, and packing pass all reuse this buffer instead
/// of re-deriving deltas per pass.
fn zigzag_deltas(values: &[i64]) -> Vec<u64> {
values
.windows(2)
Expand All @@ -114,8 +140,14 @@ fn zigzag_deltas(values: &[i64]) -> Vec<u64> {
.collect()
}

/// Append `width`-bit values (relative to `floor`) as a little-endian stream.
fn pack_bits(deltas: &[u64], floor: u64, width: u32, out: &mut Vec<u8>) {
/// Append `width`-bit deltas (relative to `floor`) as a little-endian stream.
///
/// Keep the byte-at-a-time pusher: a manual 64-bit-accumulator variant that
/// flushed whole words measured 20-30% SLOWER on the metric and contact
/// corpora (Apple Silicon, criterion vs pinned baseline) — LLVM optimizes
/// this shape better than the hand-rolled word flush with its
/// data-dependent flush branch.
fn pack_deltas(deltas: &[u64], floor: u64, width: u32, out: &mut Vec<u8>) {
let mut acc = 0_u64;
let mut filled = 0_u32;
for delta in deltas {
Expand All @@ -134,9 +166,13 @@ fn pack_bits(deltas: &[u64], floor: u64, width: u32, out: &mut Vec<u8>) {
}
}
}
if filled > 0 {
out.push(u8::try_from(acc & 0xFF).unwrap_or(0));
}
flush_partial(acc, filled, out);
}

/// Flush the trailing partial word: only the bytes carrying `filled` bits.
fn flush_partial(acc: u64, filled: u32, out: &mut Vec<u8>) {
let bytes = usize::try_from(filled.div_ceil(8)).unwrap_or(8);
out.extend_from_slice(acc.to_le_bytes().get(..bytes).unwrap_or_default());
}

/// Shift right by up to 64 bits (a 64-bit shift yields zero).
Expand All @@ -161,25 +197,43 @@ impl<'a> BitReader<'a> {
Self { bytes, bit: 0 }
}

/// Read the next `width` bits (little-endian), zero-padded past the end.
/// Read the next `width` bits (little-endian), zero-padded past the end:
/// one unaligned word load covers every width up to 57 bits, and a second
/// load stitches the rare wide read that crosses the first word's end.
fn read(&mut self, width: u32) -> u64 {
let mut value = 0_u64;
let mut got = 0_u32;
while got < width {
let byte_index = self.bit / 8;
let bit_offset = u32::try_from(self.bit % 8).unwrap_or(0);
let available = 8_u32.wrapping_sub(bit_offset);
let take = available.min(width.wrapping_sub(got));
let byte = u64::from(self.bytes.get(byte_index).copied().unwrap_or(0));
let mask = mask_of(take);
value |= (byte.wrapping_shr(bit_offset) & mask).wrapping_shl(got);
got = got.wrapping_add(take);
self.bit = self.bit.wrapping_add(usize::try_from(take).unwrap_or(0));
}
let index = self.bit / 8;
let shift = u32::try_from(self.bit % 8).unwrap_or(0);
let have = WORD_BITS.wrapping_sub(shift);
let lo = load_padded(self.bytes, index).wrapping_shr(shift);
let value = if width <= have {
lo & mask_of(width)
} else {
let hi = load_padded(self.bytes, index.wrapping_add(8));
(lo | hi.wrapping_shl(have)) & mask_of(width)
};
self.bit = self.bit.wrapping_add(usize::try_from(width).unwrap_or(0));
value
}
}

/// Load the little-endian word at byte `index`, zero-padded past the end.
fn load_padded(bytes: &[u8], index: usize) -> u64 {
bytes
.get(index..index.wrapping_add(8))
.and_then(|slice| <[u8; 8]>::try_from(slice).ok())
.map_or_else(|| tail_word(bytes, index), u64::from_le_bytes)
}

/// Little-endian fold of the partial tail starting at `index`.
fn tail_word(bytes: &[u8], index: usize) -> u64 {
bytes
.get(index..)
.unwrap_or_default()
.iter()
.rev()
.fold(0_u64, |acc, byte| acc.wrapping_shl(8) | u64::from(*byte))
}

/// Read a little-endian u64 at `offset`.
fn read_u64(block: &[u8], offset: usize) -> Result<u64, DecodeError> {
block
Expand Down
Loading
Loading