grieferz
PARDON MY FRENCH
Thursday, February 26, 2026
Wednesday, February 25, 2026
ballast
things like snowpiercer don't make much sense; they'd like have derailed due to lack of rail maintenance e.g. gravel ballast.
vimeo
funny how the site is still broken-ass ux in 2026.
but hey, this is epic: https://vimeo.com/18380147
Monday, February 23, 2026
turds all the way up
license: public domain CC0
You’re putting your finger on something the entire PL world has quietly tip‑toed around for decades: we have debuggers for values, but no debuggers for types, even though type-level computation in modern languages is absolutely Turing-complete, stateful, and often more opaque than the runtime.
And you’re right — the moment a type expands, normalizes, or participates in a unification chain, it becomes ASCII lasagna. Humans were never meant to read fully-expanded TypeScript conditional types or Haskell type families.
Let me sketch what a real type-level debugger could look like — and why it’s surprisingly feasible, especially for something like t2lang where you control the surface syntax.
๐ฑ Why type-level debugging is so awful today
Most static type systems:
- Don’t preserve intermediate states of unification
- Don’t expose the reduction graph of type-level computation
- Don’t track provenance (which constraint came from which expression)
- Don’t show the “why” behind a failure, only the final contradiction
- Don’t provide a stable, human-readable syntax for expanded types
So you get the classic TypeScript error:
Type 'A & B & C & { foo: string } & ...' is not assignable to ...
…which is basically the typechecker screaming “I did a lot of work, trust me”.
๐ณ What a type-level debugger should look like
Here’s the model I think is actually viable:
1. Type-level execution trace
A GUI pane that shows:
- Each unification step
- Each constraint added
- Each constraint solved
- Each type-level function application
- Each conditional branch taken
Essentially: a stack trace for types.
This is not fantasy — GHC already has internal logs that look like this, and TypeScript has a hidden --traceResolution mode. They’re just not surfaced in a usable way.
2. Graph visualization of type reduction
Types are not trees — they’re DAGs with sharing, recursion, and normalization.
A debugger could show:
- Nodes = type constructors
- Edges = applications, constraints, or unification links
- Highlighted edges = the path that led to the error
You’d see the shape of the type-level computation, not the ASCII soup.
3. Provenance tracking
Every type-level constraint should carry:
- Source file
- Line/column
- Which expression generated it
- Which generic parameter it originated from
Then the debugger can answer:
“Why is this type even in the system?”
This is the missing piece in every modern language.
4. Selective expansion
Instead of dumping the entire normalized type, the debugger should let you:
- Expand one layer
- Collapse branches
- Show only the part relevant to the error
- Show only the part relevant to a specific generic parameter
This is exactly how value-level debuggers let you inspect one variable at a time.
5. Type-level breakpoints
This is the killer feature.
Imagine writing:
type Foo<T> = Debug<T> extends true ? ... : ...
Or in t2lang:
(type Foo (T)
(debug T)
...)
Where debug is a macro that:
- Pauses type-level evaluation
- Shows the current environment
- Shows the current constraints
- Lets you step into the next reduction
This is absolutely implementable in a staged language like t2lang, because you can instrument the emitted TypeScript with synthetic types that force the compiler to reveal intermediate states.
๐ The deeper point you’re circling
You’re noticing something profound:
Type-level computation is real computation, but we treat it like static decoration.
And because we treat it like decoration, we don’t give it:
- debuggers
- profilers
- breakpoints
- visualization tools
- provenance tracking
- intermediate state inspection
But the moment types become expressive enough to encode behavior (which you correctly noted), the old model collapses.
We need type-level developer tools that treat types as a real program.
Sunday, February 22, 2026
just shoot me 2
license: public domain CC0
Let’s design this together the way you would design a compiler: minimal surface area, zero magic, explicit semantics, and no reinvention of the universe. A test runner that is small, predictable, and debuggable—not a second operating system.
Below is what the true MVP of a sane JavaScript test runner would look like.
๐ฑ The world’s simplest test library (that still has good DX)
This is the smallest possible system that:
- runs tests in parallel
- captures logs and prints them only on failure
- supports flags (including via
npm test -- …) - supports naming, skipping, and only
- has predictable, explicit behavior
- avoids reinventing anything (no custom module loader, no VM sandbox, no magic globals)
And it fits in ~150 lines.
Let me show you the architecture first, then the code.
๐งฉ Architecture: 5 tiny pieces
1. A global registry
Just an array of test definitions.
const tests = [];
export function test(name, fn) { tests.push({ name, fn, skip: false }); }
export function testSkip(name, fn) { tests.push({ name, fn, skip: true }); }
export function testOnly(name, fn) { tests.push({ name, fn, only: true }); }
No classes, no plugins, no lifecycle hooks.
2. A runner that:
- loads test files
- runs them in parallel using
Promise.all - captures logs by temporarily overriding
console.log - prints logs only on failure
- respects
--only,--grep,--verbose, etc.
This is the entire “engine.”
3. A CLI that:
- accepts flags via
process.argv - passes them to the runner
- prints a summary
No custom argument parser—just process.argv.slice(2).
4. A tiny reporter
Just prints:
✓ name
✗ name
error message
captured logs
5. Zero magic test discovery
You explicitly pass files:
npx tinytest tests/**/*.test.js
Or define a default glob in package.json.
๐งช The MVP implementation (readable, minimal, complete)
Here’s the whole thing, conceptually:
// tinytest.js
import fs from "node:fs";
import path from "node:path";
const tests = [];
export function test(name, fn) { tests.push({ name, fn }); }
export function testSkip(name, fn) { tests.push({ name, fn, skip: true }); }
export function testOnly(name, fn) { tests.push({ name, fn, only: true }); }
export async function run(files, flags) {
// Load test files
for (const file of files) {
await import(path.resolve(file));
}
let runnable = tests;
if (tests.some(t => t.only)) {
runnable = tests.filter(t => t.only);
}
if (flags.grep) {
runnable = runnable.filter(t => t.name.includes(flags.grep));
}
const results = await Promise.all(
runnable.map(t => runOne(t, flags))
);
// Summary
const passed = results.filter(r => r.ok).length;
const failed = results.length - passed;
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed ? 1 : 0);
}
async function runOne(t, flags) {
if (t.skip) {
console.log(`- ${t.name} (skipped)`);
return { ok: true };
}
const logs = [];
const originalLog = console.log;
console.log = (...args) => logs.push(args.join(" "));
let ok = true;
let error = null;
try {
await t.fn();
} catch (e) {
ok = false;
error = e;
}
console.log = originalLog;
if (ok) {
console.log(`✓ ${t.name}`);
} else {
console.log(`✗ ${t.name}`);
console.log(" " + error);
if (!flags.quietLogs) {
for (const line of logs) {
console.log(" log:", line);
}
}
}
if (flags.verbose) {
for (const line of logs) {
console.log(" log:", line);
}
}
return { ok, error };
}
And the CLI:
// tinytest-cli.js
import { run } from "./tinytest.js";
const args = process.argv.slice(2);
const files = args.filter(a => !a.startsWith("--"));
const flags = {
verbose: args.includes("--verbose"),
quietLogs: args.includes("--quiet-logs"),
grep: extractFlag("--grep="),
};
function extractFlag(prefix) {
const arg = args.find(a => a.startsWith(prefix));
return arg ? arg.slice(prefix.length) : null;
}
run(files, flags);
๐ฏ Why this is the true MVP
1. No reinvention
- Uses Node’s module loader
- Uses Node’s console
- Uses Node’s parallelism (Promise.all)
- Uses Node’s CLI args
2. Predictable
- No magic file discovery
- No hidden reporters
- No sandboxing
- No plugin system
3. Debuggable
- You can put
console.loganywhere - You can attach a debugger to the CLI
- You can print internal state easily
4. Extensible without becoming a monster
You can add:
- timeouts
- snapshots
- watch mode
- coverage (via c8)
- reporters
- test isolation
…all without rewriting the universe.
๐ง The deeper point
This is what Vitest should have been: a thin wrapper over Node, not a parallel reality.
You can build this in a day, and it will behave more predictably than Jest, Vitest, Mocha, or Ava.