/** * Executable form of scopes.md §6. * * npx tsc --noEmit --strict --lib es2023 scopes.variance.ts * * Silence means the rule holds: reads accept either scope, single-scope commits * pass, and mixed-scope commits are the ONLY thing rejected. The three * `@ts-expect-error` lines fail the compile if they ever start type-checking, * so this catches the enforcement being weakened as well as broken. * * The variance split is the part that is easy to get backwards. Making addresses * invariant too breaks every reader — getValue, scanValues, readList and * everything downstream — and the tempting fix, making each reader generic over * Sc, propagates a type parameter through the whole storage stack to express * what variance gives for free. */ declare const tag: unique symbol; type SessionScope = { readonly kind: "session" }; type EphemeralScope = { readonly kind: "ephemeral" }; type Scope = SessionScope | EphemeralScope; /** Addresses: COVARIANT — Sc only in return position. Readers take any scope. */ interface ScopedAddress { readonly [tag]?: () => Sc } /** Writes: INVARIANT — Sc in both positions. Mixed commits cannot unify. */ interface Scoped { readonly [tag]?: (s: Sc) => Sc } interface Value extends ScopedAddress { k: "v" } interface ValueList extends ScopedAddress { k: "l" } interface VSet extends Scoped { kind: "value"; op: "set" } interface VDel extends Scoped { kind: "value"; op: "delete" } interface LApp extends Scoped { kind: "list"; op: "append" } interface EntryWrite { kind: "entry" } interface RetireWrite extends Scoped { kind: "scope" } type Write = | VSet | VDel | LApp | (Sc extends SessionScope ? EntryWrite | RetireWrite : never); declare function setValue(a: Value, v: T): VSet; declare function deleteValue(a: Value): VDel; declare function appendList(a: ValueList, v: T): LApp; declare function retireScope(id: string): RetireWrite; declare function insertEntry(e: unknown): EntryWrite; declare function commit(w: readonly Write[]): void; declare function getValue(a: Value): T; // reader: any scope const laneState = null as unknown as Value; // session const pendingOut = null as unknown as ValueList; // ephemeral const toolMemo = null as unknown as Value; // READS accept either scope — that is what covariance buys. getValue(laneState); getValue(toolMemo); // Single-scope commits pass. commit([setValue(laneState, "a")]); commit([setValue(laneState, "a"), insertEntry({}), retireScope("op_1")]); commit([appendList(pendingOut, "x"), setValue(toolMemo, "m")]); // @ts-expect-error mixed scopes in one transaction commit([setValue(laneState, "a"), appendList(pendingOut, "x")]); // @ts-expect-error a retire is session-only, so it cannot join an ephemeral commit commit([appendList(pendingOut, "x"), retireScope("op_1")]); // @ts-expect-error entries are session-only commit([insertEntry({})]);