Skip to content

Instantly share code, notes, and snippets.

@mikesol
Last active February 17, 2026 06:40
Show Gist options
  • Select an option

  • Save mikesol/66e07b2f36fd7239b02093f36ec2448f to your computer and use it in GitHub Desktop.

Select an option

Save mikesol/66e07b2f36fd7239b02093f36ec2448f to your computer and use it in GitHub Desktop.
Spike: end-to-end phantom kind propagation (validates #233)
/**
* Spike: end-to-end phantom kind propagation with branded Interpreter.
* Validates the approach described in #233 on a small scale.
*
* Run: npx tsc --noEmit (uses tsconfig.json)
*/
// ============================================================
// Typed node interfaces
// ============================================================
interface TypedNode<T = unknown> {
readonly kind: string;
readonly __T?: T;
}
interface NumAddNode extends TypedNode<number> {
readonly kind: "num/add";
left: TypedNode<number>;
right: TypedNode<number>;
}
interface NumSubNode extends TypedNode<number> {
readonly kind: "num/sub";
left: TypedNode<number>;
right: TypedNode<number>;
}
interface StrUpperNode extends TypedNode<string> {
readonly kind: "str/upper";
inner: TypedNode<string>;
}
// ============================================================
// NodeTypeMap — the global registry
// In the real codebase this uses declaration merging.
// Here we define it directly for the spike.
// ============================================================
interface NodeTypeMap {
"num/add": NumAddNode;
"num/sub": NumSubNode;
"str/upper": StrUpperNode;
}
// ============================================================
// Core type machinery
// ============================================================
type FoldYield = TypedNode;
type IsAny<T> = 0 extends 1 & T ? true : false;
type ExtractNodeParam<F> = F extends (node: infer N, ...args: any[]) => any ? N : unknown;
type RejectAnyParam<_K extends string, H> = IsAny<ExtractNodeParam<H>> extends true ? never : H;
type Handler<N extends TypedNode<any>> = N extends TypedNode<infer T>
? (node: N) => AsyncGenerator<FoldYield, T, unknown>
: never;
/** Raw handler map — the structural shape, without the brand. */
type InterpreterHandlers<K extends string> = {
[key in K]: key extends keyof NodeTypeMap ? Handler<NodeTypeMap[key]> : never;
};
// ============================================================
// Branded Interpreter — can ONLY be produced by factory functions
// ============================================================
declare const interpreterBrand: unique symbol;
/**
* Branded interpreter type. The unique symbol makes it impossible to
* construct via annotation — you must go through a factory function
* (defineInterpreter or definePlugin) which rejects any-typed handlers.
*/
type Interpreter<K extends string> = InterpreterHandlers<K> & {
readonly [interpreterBrand]: K;
};
/**
* Factory: create a branded Interpreter with any-rejection.
* This is the ONLY way to produce an Interpreter<K>.
*/
function defineInterpreter<K extends string>() {
return <T extends InterpreterHandlers<K>>(
handlers: T & {
[P in K]: P extends keyof T ? RejectAnyParam<P, T[P]> : never;
},
): Interpreter<K> => {
return handlers as unknown as Interpreter<K>; // one trusted cast
};
}
// ============================================================
// PluginDefinition — K is the literal union of node kinds
// ============================================================
interface PluginDefinition<K extends string = string, T = unknown> {
name: string;
nodeKinds: K[];
defaultInterpreter: Interpreter<K>;
build: () => T;
}
/**
* Factory: create a PluginDefinition with branded interpreter.
* Infers K from nodeKinds, rejects any in handlers.
*/
function definePlugin<const Kinds extends string[], I extends InterpreterHandlers<Kinds[number]>, T>(def: {
name: string;
nodeKinds: Kinds;
defaultInterpreter: I & {
[P in Kinds[number]]: P extends keyof I ? RejectAnyParam<P, I[P]> : never;
};
build: () => T;
}): PluginDefinition<Kinds[number], T> {
return {
...def,
defaultInterpreter: def.defaultInterpreter as unknown as Interpreter<Kinds[number]>,
} as PluginDefinition<Kinds[number], T>;
}
// ============================================================
// Program carries phantom K
// ============================================================
interface Program<K extends string = string> {
ast: TypedNode;
hash: string;
readonly __kinds?: K;
}
// ============================================================
// mvfm() infers K from plugins
// ============================================================
type ExtractKinds<P> = P extends PluginDefinition<infer K, any> ? K : never;
function mvfm<P extends PluginDefinition<any, any>[]>(
...plugins: P
): (builder: ($: any) => any) => Program<ExtractKinds<P[number]>> {
void plugins;
return (_builder) => ({ ast: { kind: "dummy" }, hash: "dummy" }) as any;
}
// ============================================================
// defaults() returns branded Interpreter<K>
// ============================================================
type AppFn<K extends string> = (builder: ($: any) => any) => Program<K>;
function defaults<K extends string>(_app: AppFn<K>): Interpreter<K> {
return {} as Interpreter<K>; // merges plugin defaultInterpreters internally
}
// ============================================================
// foldAST — requires branded Interpreter<K>. No RejectAnyParam
// needed here because the brand guarantees the interpreter was
// produced by a factory that already rejected any.
// ============================================================
async function foldAST<K extends string>(
interpreter: Interpreter<K>,
program: Program<K>,
): Promise<unknown> {
void interpreter;
void program;
return undefined;
}
// ============================================================
// Merging interpreters (spread composition)
// ============================================================
function mergeInterpreters<A extends string, B extends string>(
a: Interpreter<A>,
b: Interpreter<B>,
): Interpreter<A | B> {
return { ...a, ...b } as Interpreter<A | B>; // trusted cast
}
// ============================================================
// Plugin definitions
// ============================================================
const num = definePlugin({
name: "num",
nodeKinds: ["num/add", "num/sub"],
defaultInterpreter: {
// biome-ignore lint/correctness/useYield: spike
"num/add": async function* (node: NumAddNode) {
return node.left as unknown as number;
},
// biome-ignore lint/correctness/useYield: spike
"num/sub": async function* (node: NumSubNode) {
return node.left as unknown as number;
},
},
build: () => ({ add: null, sub: null }),
});
const str = definePlugin({
name: "str",
nodeKinds: ["str/upper"],
defaultInterpreter: {
// biome-ignore lint/correctness/useYield: spike
"str/upper": async function* (_node: StrUpperNode) {
return "";
},
},
build: () => ({ upper: null }),
});
// ============================================================
// Also works: defineInterpreter standalone
// ============================================================
const _numInterp = defineInterpreter<"num/add" | "num/sub">()({
// biome-ignore lint/correctness/useYield: spike
"num/add": async function* (node: NumAddNode) {
return node.left as unknown as number;
},
// biome-ignore lint/correctness/useYield: spike
"num/sub": async function* (node: NumSubNode) {
return node.left as unknown as number;
},
});
// ============================================================
// POSITIVE TESTS — these must compile
// ============================================================
// Multi-plugin: K = "num/add" | "num/sub" | "str/upper"
const app = mvfm(num, str);
const prog = app(($) => $.add(1, 2));
const interp = defaults(app);
void foldAST(interp, prog);
// Single plugin
const numApp = mvfm(num);
const numProg = numApp(($) => $.add(1, 2));
const numInterp = defaults(numApp);
void foldAST(numInterp, numProg);
// Merge interpreters
const merged = mergeInterpreters(
defineInterpreter<"num/add" | "num/sub">()({
// biome-ignore lint/correctness/useYield: spike
"num/add": async function* (node: NumAddNode) {
return node.left as unknown as number;
},
// biome-ignore lint/correctness/useYield: spike
"num/sub": async function* (node: NumSubNode) {
return node.left as unknown as number;
},
}),
defineInterpreter<"str/upper">()({
// biome-ignore lint/correctness/useYield: spike
"str/upper": async function* (_node: StrUpperNode) {
return "";
},
}),
);
void foldAST(merged, prog);
// ============================================================
// NEGATIVE TESTS — these must NOT compile
// ============================================================
// --- Brand prevents constructing Interpreter via annotation ---
// @ts-expect-error brand prevents raw construction — missing [interpreterBrand]
const _brandBlocked: Interpreter<"num/add"> = {
// biome-ignore lint/correctness/useYield: spike
"num/add": async function* (node: NumAddNode) {
return node.left as unknown as number;
},
};
// --- Brand prevents ANY handler via annotation (even correct types) ---
// @ts-expect-error brand prevents raw construction even with correct handler types
const _brandBlockedCorrectTypes: Interpreter<"num/add" | "num/sub"> = {
// biome-ignore lint/correctness/useYield: spike
"num/add": async function* (node: NumAddNode) {
return node.left as unknown as number;
},
// biome-ignore lint/correctness/useYield: spike
"num/sub": async function* (node: NumSubNode) {
return node.left as unknown as number;
},
};
// --- node:any rejected in defineInterpreter ---
const _badAny = defineInterpreter<"num/add">()({
// @ts-expect-error node:any rejected by RejectAnyParam
// biome-ignore lint/correctness/useYield: spike
"num/add": async function* (node: any) {
return node;
},
});
// --- wrong node type rejected in defineInterpreter ---
const _badWrongType = defineInterpreter<"num/add">()({
// @ts-expect-error wrong node type
// biome-ignore lint/correctness/useYield: spike
"num/add": async function* (node: StrUpperNode) {
return node.inner;
},
});
// --- missing handler rejected in defineInterpreter ---
// @ts-expect-error missing "num/sub" handler
const _badMissing = defineInterpreter<"num/add" | "num/sub">()({
// biome-ignore lint/correctness/useYield: spike
"num/add": async function* (node: NumAddNode) {
return node.left as unknown as number;
},
});
// --- node:any rejected in definePlugin ---
const _badPlugin = definePlugin({
name: "bad",
nodeKinds: ["num/add"],
defaultInterpreter: {
// @ts-expect-error node:any rejected in definePlugin
// biome-ignore lint/correctness/useYield: spike
"num/add": async function* (node: any) {
return node;
},
},
build: () => ({ add: null }),
});
// --- unregistered kind produces never in definePlugin ---
const _unregPlugin = definePlugin({
name: "unreg",
nodeKinds: ["fake/kind"],
defaultInterpreter: {
// @ts-expect-error unregistered kind maps to never
// biome-ignore lint/correctness/useYield: spike
"fake/kind": async function* (_node: TypedNode) {
return undefined;
},
},
build: () => ({}),
});
// --- foldAST rejects unbranded interpreter ---
void foldAST(
// @ts-expect-error unbranded object rejected by foldAST
{
// biome-ignore lint/correctness/useYield: spike
"num/add": async function* (node: NumAddNode) {
return node.left as unknown as number;
},
// biome-ignore lint/correctness/useYield: spike
"num/sub": async function* (node: NumSubNode) {
return node.left as unknown as number;
},
},
numProg,
);
{
"compilerOptions": {
"strict": true,
"noEmit": true,
"target": "ES2022",
"lib": ["ES2022"],
"module": "ES2022",
"moduleResolution": "bundler"
},
"include": ["spike.ts"]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment