typchk or smthng idk

This commit is contained in:
SimonFJ20 2025-02-03 15:04:06 +01:00
parent 1f9cbea832
commit 60efd931f5
11 changed files with 557 additions and 184 deletions

View File

@ -62,7 +62,9 @@ export type ItemKind =
| { tag: "type_alias" } & TypeAliasItem;
export type ModBlockItem = { block: Block };
export type ModFileItem = { filePath: string; file?: CtxFile };
export type ModFileItem = { filePath: string; file?: CtxFile; ast?: File };
export type EnumItem = { variants: Variant[] };
export type StructItem = { data: VariantData };

33
compiler/ast/to_string.ts Normal file
View File

@ -0,0 +1,33 @@
import { Ctx } from "../ctx.ts";
import { exhausted, todo } from "../util.ts";
import { Block, Item } from "./ast.ts";
export function itemToString(ctx: Ctx, item: Item): string {
const ident = ctx.identText(item.ident.id);
const k = item.kind;
switch (k.tag) {
case "error":
return `<error>`;
case "mod_block": {
const block = blockToString(ctx, k.block);
return `mod ${item} ${block}`;
}
case "mod_file":
return todo();
case "enum":
return todo();
case "struct":
return todo();
case "fn":
return todo();
case "use":
return todo();
case "type_alias":
return todo();
}
return exhausted(k);
}
export function blockToString(ctx: Ctx, block: Block): string {
return todo();
}

185
compiler/check/checker.ts Normal file
View File

@ -0,0 +1,185 @@
import * as ast from "../ast/mod.ts";
import { Ctx, File } from "../ctx.ts";
import { Span } from "../diagnostics.ts";
import { Resols } from "../resolve/resolver.ts";
import { tyToString } from "../ty/to_string.ts";
import { Ty } from "../ty/ty.ts";
import { exhausted, Res, todo } from "../util.ts";
export class Checker {
private itemTys = new Map<number, Ty>();
private exprTys = new Map<number, Ty>();
private tyTys = new Map<number, Ty>();
private currentFile: File;
public constructor(
private ctx: Ctx,
private entryFileAst: ast.File,
private resols: Resols,
) {
this.currentFile = ctx.entryFile();
}
private checkBlock(block: ast.Block, expected: Ty): Ty {
this.checkStmts(block.stmts);
return block.expr &&
this.checkExpr(block.expr, expected) ||
Ty({ tag: "null" });
}
private checkStmts(stmts: ast.Stmt[]) {
}
public fnItemTy(item: ast.Item, kind: ast.FnItem): Ty {
return this.itemTys.get(item.id) || this.checkFnItem(item, kind);
}
private checkFnItem(item: ast.Item, kind: ast.FnItem): Ty {
const params = kind.params.map((param) => this.tyTy(param.ty));
const returnTy = kind.returnTy && this.tyTy(kind.returnTy) ||
Ty({ tag: "null" });
return Ty({ tag: "fn", item, kind, params, returnTy });
}
public exprTy(expr: ast.Expr): Ty {
return this.exprTys.get(expr.id) ||
this.checkExpr(expr, Ty({ tag: "unknown" }));
}
private checkExpr(expr: ast.Expr, expected: Ty): Ty {
const k = expr.kind;
switch (k.tag) {
case "error":
return Ty({ tag: "error" });
case "path":
return this.checkPathExpr(expr, k, expected);
case "null":
return todo();
case "int":
return todo();
case "bool":
return todo();
case "str":
return todo();
case "group":
return todo();
case "array":
return todo();
case "repeat":
return todo();
case "struct":
return todo();
case "ref":
return todo();
case "deref":
return todo();
case "elem":
return todo();
case "field":
return todo();
case "index":
return todo();
case "call":
return todo();
case "unary":
return todo();
case "binary":
return todo();
case "block":
return todo();
case "if":
return todo();
case "loop":
return todo();
case "while":
return todo();
case "for":
return todo();
case "c_for":
return todo();
}
exhausted(k);
}
private checkPathExpr(
expr: ast.Expr,
kind: ast.PathExpr,
expected: Ty,
): Ty {
const res = this.resols.exprRes(expr.id);
switch (res.kind.tag) {
case "error":
return Ty({ tag: "error" });
case "fn": {
const fn = res.kind.item;
const ty = this.fnItemTy(fn, res.kind.kind);
const resu = this.resolveTys(ty, expected);
if (!resu.ok) {
this.report(resu.val, expr.span);
return Ty({ tag: "error" });
}
return resu.val;
}
case "local": {
const ty = this.exprTy(expr);
const resu = this.resolveTys(ty, expected);
if (!resu.ok) {
this.report(resu.val, expr.span);
return Ty({ tag: "error" });
}
return resu.val;
}
}
exhausted(res.kind);
}
private tyTy(ty: ast.Ty): Ty {
return this.tyTys.get(ty.id) ||
this.checkTy(ty);
}
private checkTy(ty: ast.Ty): Ty {
return todo();
}
private report(msg: string, span: Span) {
this.ctx.report({
severity: "error",
file: this.currentFile,
span,
msg,
});
}
private resolveTys(a: Ty, b: Ty): Res<Ty, string> {
const as = tyToString(this.ctx, a);
const bs = tyToString(this.ctx, b);
const incompat = () =>
Res.Err(
`type '${as}' not compatible with type '${bs}'`,
);
switch (a.kind.tag) {
case "error":
return Res.Ok(b);
case "unknown":
return Res.Ok(b);
case "null": {
if (b.kind.tag !== "null") {
return incompat();
}
return Res.Ok(a);
}
case "fn": {
if (b.kind.tag !== "fn") {
return incompat();
}
if (b.kind.item.id === a.kind.item.id) {
return incompat();
}
return Res.Ok(a);
}
}
exhausted(a.kind);
}
}

View File

@ -4,6 +4,7 @@ import * as ast from "./ast/mod.ts";
import { Ctx } from "./ctx.ts";
import { File } from "./ctx.ts";
import { Resolver } from "./resolve/resolver.ts";
import { Checker } from "./check/checker.ts";
async function main() {
const filePath = Deno.args[0];
@ -37,13 +38,11 @@ export class PackCompiler {
) {}
public async compile() {
await FileTreeAstCollector
const [entryFile, entryFileAst] = await FileTreeAstCollector
.fromEntryFile(this.ctx, this.astCx, this.entryFilePath)
.collect();
// this.ctx.printAsts();
const entryFile = this.ctx.entryFile();
const entryFileAst = this.ctx.fileInfo(entryFile).ast!;
new Resolver(this.ctx, entryFileAst).resolve();
const resols = new Resolver(this.ctx, entryFileAst).resolve();
const checker = new Checker(this.ctx, entryFileAst, resols);
}
public enableDebug() {
@ -80,7 +79,7 @@ export class FileTreeAstCollector implements ast.Visitor<[_P]> {
);
}
public async collect(): Promise<File> {
public async collect(): Promise<[File, ast.File]> {
const text = await Deno.readTextFile(this.absPath);
const file = this.ctx.addFile(
this.ident,
@ -93,7 +92,7 @@ export class FileTreeAstCollector implements ast.Visitor<[_P]> {
this.ctx.addFileAst(file, fileAst);
ast.visitFile(this, fileAst, { file });
await this.subFilePromise;
return file;
return [file, fileAst];
}
visitModFileItem(
@ -115,7 +114,7 @@ export class FileTreeAstCollector implements ast.Visitor<[_P]> {
});
Deno.exit(1);
}
const modFile = await new FileTreeAstCollector(
const [modFile, modAst] = await new FileTreeAstCollector(
this.ctx,
this.astCx,
file,
@ -125,6 +124,7 @@ export class FileTreeAstCollector implements ast.Visitor<[_P]> {
)
.collect();
kind.file = modFile;
kind.ast = modAst;
});
return "stop";
}

40
compiler/middle/mir.ts Normal file
View File

@ -0,0 +1,40 @@
import { Span } from "../diagnostics.ts";
export type Stmt = {
kind: StmtKind;
};
export type StmtKind =
| { tag: "error" }
| { tag: "assign" } & AssignStmt
| { tag: "fake_read" } & FakeReadStmt
| { tag: "deinit" } & DeinitStmt
| { tag: "live" } & LiveStmt
| { tag: "dead" } & DeadStmt
| { tag: "mention" } & MentionStmt;
export type AssignStmt = { place: Place; rval: RVal };
export type FakeReadStmt = { place: Place };
export type DeinitStmt = { place: Place };
export type LiveStmt = { local: Local };
export type DeadStmt = { local: Local };
export type MentionStmt = { place: Place };
export type Place = {
local: Local;
proj: ProjElem[];
};
// https://doc.rust-lang.org/beta/nightly-rustc/rustc_middle/mir/type.PlaceElem.html
export type ProjElem =
| { tag: "deref" }
| { tag: "repeat" }
| { tag: "field"; fieldIdx: number }
| { tag: "index": local: Local }
| { tag: }
// https://doc.rust-lang.org/beta/nightly-rustc/rustc_middle/mir/enum.Rvalue.html
export type RVal = {};
export type Local = {};

View File

@ -9,3 +9,4 @@ fn main() {
let c = add(a, b);
}

View File

@ -1,94 +1,130 @@
import * as ast from "../ast/mod.ts";
import { IdentId, idKey, Key } from "../ctx.ts";
import { Res } from "../util.ts";
type Ident = Key<IdentId>;
export interface Syms {
getVal(ident: ast.Ident): Resolve;
getTy(ident: ast.Ident): Resolve;
export class Ribs {
private tyRibs: Rib[] = [];
private valRibs: Rib[] = [];
defVal(ident: ast.Ident, kind: ResolveKind): Res<void, Redef>;
defTy(ident: ast.Ident, kind: ResolveKind): Res<void, Redef>;
}
private constructor() {}
export type Resolve = {
ident: ast.Ident;
kind: ResolveKind;
};
public static withRootMod(): Ribs {
const ribs = new Ribs();
ribs.pushRib({ tag: "mod", mod: { items: new Map() } });
return ribs;
export type ResolveKind =
| { tag: "error" }
| { tag: "fn"; item: ast.Item; kind: ast.FnItem }
| { tag: "local" };
export const ResolveError = (ident: ast.Ident): Resolve => ({
ident,
kind: { tag: "error" },
});
export type Redef = {
ident: ast.Ident;
};
export class SymsOneNsTab {
private defs = new Map<Key<IdentId>, Resolve>();
public get(ident: ast.Ident): Resolve | undefined {
return this.defs.get(idKey(ident.id))!;
}
public pushRib(kind: RibKind) {
this.tyRibs.push({ bindings: new Map(), kind });
this.valRibs.push({ bindings: new Map(), kind });
}
public hasTy(ident: IdentId): boolean {
return this.tyRibs.at(-1)!.bindings.has(idKey(ident));
}
public defTy(ident: IdentId, res: Res) {
this.tyRibs.at(-1)!.bindings.set(idKey(ident), res);
}
public hasVal(ident: IdentId): boolean {
return this.valRibs.at(-1)!.bindings.has(idKey(ident));
}
public val(ident: IdentId) {
}
public defVal(ident: IdentId, res: Res) {
this.valRibs.at(-1)!.bindings.set(idKey(ident), res);
}
public checkpoint(): number {
return this.tyRibs.length;
}
public returnToCheckpoint(checkpoint: number) {
this.tyRibs = this.tyRibs.slice(checkpoint, this.tyRibs.length);
this.valRibs = this.valRibs.slice(checkpoint, this.valRibs.length);
}
public nearestMod(): Mod {
return [
this.tyRibs
.toReversed()
.find((r) => r.kind.tag === "mod")!,
]
.map((r) => (r.kind.tag === "mod" && r.kind.mod) as Mod)[0];
public def(ident: ast.Ident, kind: ResolveKind): Res<void, Redef> {
if (this.defs.has(idKey(ident.id))) {
return Res.Err({ ident: this.defs.get(idKey(ident.id))!.ident });
}
this.defs.set(idKey(ident.id), { ident, kind });
return Res.Ok(undefined);
}
}
export type Mod = {
parent?: Mod;
items: Map<Ident, Res>;
};
export class SymsNsTab {
private vals = new SymsOneNsTab();
private tys = new SymsOneNsTab();
export type Rib = {
bindings: Map<Ident, Res>;
kind: RibKind;
};
public getVal(ident: ast.Ident): Resolve | undefined {
return this.vals.get(ident);
}
public getTy(ident: ast.Ident): Resolve | undefined {
return this.tys.get(ident);
}
export type RibKind =
| { tag: "normal" }
| { tag: "fn" }
| { tag: "item" }
| { tag: "mod"; mod: Mod };
public defVal(ident: ast.Ident, kind: ResolveKind): Res<void, Redef> {
return this.vals.def(ident, kind);
}
public defTy(ident: ast.Ident, kind: ResolveKind): Res<void, Redef> {
return this.tys.def(ident, kind);
}
}
export type Res =
| { tag: "def"; def: Def }
| { tag: "local"; id: number };
export class RootSyms implements Syms {
private syms = new SymsNsTab();
export type Def = {
type: DefType;
id: number;
};
getVal(ident: ast.Ident): Resolve {
return this.syms.getVal(ident) || ResolveError(ident);
}
getTy(ident: ast.Ident): Resolve {
return this.syms.getTy(ident) || ResolveError(ident);
}
defVal(ident: ast.Ident, kind: ResolveKind): Res<void, Redef> {
return this.syms.defVal(ident, kind);
}
defTy(ident: ast.Ident, kind: ResolveKind): Res<void, Redef> {
return this.syms.defTy(ident, kind);
}
}
export class FnSyms implements Syms {
private syms = new SymsNsTab();
public constructor(
private parent: Syms,
) {}
getVal(ident: ast.Ident): Resolve {
const res = this.syms.getVal(ident) || this.parent.getVal(ident);
if (res.kind.tag === "local") {
return ResolveError(ident);
}
return res;
}
getTy(ident: ast.Ident): Resolve {
return this.syms.getTy(ident) || this.parent.getTy(ident);
}
defVal(ident: ast.Ident, kind: ResolveKind): Res<void, Redef> {
return this.syms.defVal(ident, kind);
}
defTy(ident: ast.Ident, kind: ResolveKind): Res<void, Redef> {
return this.syms.defTy(ident, kind);
}
}
export class LocalSyms implements Syms {
private syms = new SymsNsTab();
public constructor(
private parent: Syms,
) {}
getVal(ident: ast.Ident): Resolve {
return this.syms.getVal(ident) || this.parent.getVal(ident);
}
getTy(ident: ast.Ident): Resolve {
return this.syms.getTy(ident) || this.parent.getTy(ident);
}
defVal(ident: ast.Ident, kind: ResolveKind): Res<void, Redef> {
return this.syms.defVal(ident, kind);
}
defTy(ident: ast.Ident, kind: ResolveKind): Res<void, Redef> {
return this.syms.defTy(ident, kind);
}
}
export type DefType =
| "mod"
| "enum"
| "struct"
| "variant"
| "ty_alias"
| "ty_param"
| "fn"
| "use"
| "field";

View File

@ -1,60 +1,67 @@
import * as ast from "../ast/mod.ts";
import { Ctx, File, IdentId, idKey, Key } from "../ctx.ts";
import { todo } from "../util.ts";
import { Def, DefType, Mod, Res, Ribs } from "./cx.ts";
import { Ctx, File } from "../ctx.ts";
import { exhausted, todo } from "../util.ts";
import {
FnSyms,
LocalSyms,
Resolve,
ResolveError,
RootSyms,
Syms,
} from "./cx.ts";
export class Resols {
public constructor(
private exprResols: Map<number, Resolve>,
) {}
public exprRes(id: number): Resolve {
if (!this.exprResols.has(id)) {
throw new Error();
}
return this.exprResols.get(id)!;
}
}
export class Resolver implements ast.Visitor {
private ribs = Ribs.withRootMod();
private currentFile!: File;
private rootSyms = new RootSyms();
private syms: Syms = this.rootSyms;
private exprResols = new Map<number, Resolve>();
public constructor(
private ctx: Ctx,
private entryFileAst: ast.File,
) {}
public resolve() {
public resolve(): Resols {
ast.visitFile(this, this.entryFileAst);
return new Resols(
this.exprResols,
);
}
visitFile(file: ast.File): ast.VisitRes {
this.currentFile = this.entryFileAst.file;
this.currentFile = file.file;
ast.visitStmts(this, file.stmts);
this.resolveFnBlocks();
this.visitFnBodies();
}
visitLetStmt(stmt: ast.Stmt, kind: ast.LetStmt): ast.VisitRes {
kind.expr && ast.visitExpr(this, kind.expr);
kind.ty && ast.visitTy(this, kind.ty);
this.ribs.pushRib({ tag: "normal" });
kind.expr && ast.visitExpr(this, kind.expr);
this.syms = new LocalSyms(this.syms);
ast.visitPat(this, kind.pat);
return "stop";
}
visitModBlockItem(item: ast.Item, kind: ast.ModBlockItem): ast.VisitRes {
const mod: Mod = {
parent: this.ribs.nearestMod(),
items: new Map(),
};
const ribPoint = this.ribs.checkpoint();
ast.visitBlock(this, kind.block);
this.ribs.pushRib({ tag: "mod", mod });
this.ribs.returnToCheckpoint(ribPoint);
this.defineTy(item.ident, this.defMod(item.ident.id, item, mod));
return "stop";
todo();
}
visitModFileItem(item: ast.Item, kind: ast.ModFileItem): ast.VisitRes {
const mod: Mod = {
parent: this.ribs.nearestMod(),
items: new Map(),
};
const ribPoint = this.ribs.checkpoint();
const fileAst = this.ctx.fileInfo(kind.file!).ast!;
ast.visitFile(this, fileAst);
this.ribs.pushRib({ tag: "mod", mod });
this.ribs.returnToCheckpoint(ribPoint);
this.defineTy(item.ident, this.defMod(item.ident.id, item, mod));
return "stop";
ast.visitFile(this, kind.ast!);
todo();
}
visitEnumItem(item: ast.Item, kind: ast.EnumItem): ast.VisitRes {
@ -65,29 +72,55 @@ export class Resolver implements ast.Visitor {
todo();
}
private fnBlocksToResolve: [ast.Item, ast.FnItem][] = [];
private fnBodiesToCheck: [ast.Item, ast.FnItem][] = [];
visitFnItem(item: ast.Item, kind: ast.FnItem): ast.VisitRes {
this.defineVal(item.ident, this.defFn(item.ident.id, item, kind));
this.fnBlocksToResolve.push([item, kind]);
this.syms.defVal(item.ident, { tag: "fn", item, kind });
this.fnBodiesToCheck.push([item, kind]);
return "stop";
}
private resolveFnBlocks() {
for (const [item, kind] of this.fnBlocksToResolve) {
const ribPoint = this.ribs.checkpoint();
this.ribs.pushRib({ tag: "fn" });
private visitFnBodies() {
for (const [_item, kind] of this.fnBodiesToCheck) {
const outerSyms = this.syms;
this.syms = new FnSyms(this.syms);
this.syms = new LocalSyms(this.syms);
for (const param of kind.params) {
ast.visitParam(this, param);
}
ast.visitBlock(this, kind.body!);
this.ribs.returnToCheckpoint(ribPoint);
this.syms = outerSyms;
}
this.fnBlocksToResolve = [];
this.fnBodiesToCheck = [];
}
visitPathExpr(expr: ast.Expr, kind: ast.PathExpr): ast.VisitRes {
todo();
if (kind.path.segments.length === 1) {
const res = this.syms.getVal(kind.path.segments[0].ident);
switch (res.kind.tag) {
case "error":
return "stop";
case "fn":
this.exprResols.set(expr.id, res);
return "stop";
case "local":
this.exprResols.set(expr.id, res);
return "stop";
}
exhausted(res.kind);
}
const pathRes = this.resolveInnerPath(kind.path);
switch (pathRes.kind.tag) {
case "error":
todo();
return "stop";
case "fn":
todo();
return "stop";
case "local":
todo();
return "stop";
}
exhausted(pathRes.kind);
}
visitUseItem(item: ast.Item, kind: ast.UseItem): ast.VisitRes {
@ -99,70 +132,70 @@ export class Resolver implements ast.Visitor {
}
visitBindPat(pat: ast.Pat, kind: ast.BindPat): ast.VisitRes {
this.ribs.defVal(kind.ident.id, { tag: "local", id: pat.id });
return "stop";
const res = this.syms.defVal(kind.ident, { tag: "local" });
if (!res.ok) {
const text = this.ctx.identText(kind.ident.id);
this.ctx.report({
severity: "error",
file: this.currentFile,
span: kind.ident.span,
msg: `redefinition of value '${text}'`,
});
}
}
visitPathPat(pat: ast.Pat, kind: ast.PathPat): ast.VisitRes {
todo();
return "stop";
}
visitBlock(block: ast.Block): ast.VisitRes {
ast.visitStmts(this, block.stmts);
this.visitFnBodies();
block.expr && ast.visitExpr(this, block.expr);
this.resolveFnBlocks();
return "stop";
}
private defIdCounter = 0;
private modDefs = new Map<number, [ast.Item, Mod]>();
private modDef(id: number): [ast.Item, Mod] {
return this.modDefs.get(id)!;
}
private defMod(_ident: IdentId, item: ast.Item, mod: Mod): Res {
const id = this.defIdCounter++;
this.modDefs.set(id, [item, mod]);
return { tag: "def", def: { id, type: "mod" } };
private resolveInnerPath(path: ast.Path): Resolve {
const res = path.segments.slice(1, path.segments.length)
.reduce((innerRes, seg) => {
const k = innerRes.kind;
switch (k.tag) {
case "error":
return innerRes;
case "fn":
this.ctx.report({
severity: "error",
file: this.currentFile,
span: seg.ident.span,
msg: "function, not pathable",
});
return ResolveError(seg.ident);
case "local":
this.ctx.report({
severity: "error",
file: this.currentFile,
span: seg.ident.span,
msg: "local variable, not pathable",
});
return ResolveError(seg.ident);
}
exhausted(k);
}, this.syms.getTy(path.segments[0].ident));
return res;
}
private fnDefs = new Map<number, [ast.Item, ast.FnItem]>();
private fnDef(id: number): [ast.Item, ast.FnItem] {
return this.fnDefs.get(id)!;
}
private defFn(_ident: IdentId, item: ast.Item, kind: ast.FnItem): Res {
const id = this.defIdCounter++;
this.fnDefs.set(id, [item, kind]);
return { tag: "def", def: { id, type: "fn" } };
}
private defineTy(ident: ast.Ident, res: Res) {
if (this.ribs.hasTy(ident.id)) {
const text = this.ctx.identText(ident.id);
this.ctx.report({
severity: "error",
file: this.currentFile,
span: ident.span,
msg: `redefinition of type '${text}'`,
});
return;
}
this.ribs.defTy(ident.id, res);
}
private defineVal(ident: ast.Ident, res: Res) {
if (this.ribs.hasVal(ident.id)) {
console.log(this.ribs);
const text = this.ctx.identText(ident.id);
this.ctx.report({
severity: "error",
file: this.currentFile,
span: ident.span,
msg: `redefinition of value '${text}'`,
});
return;
}
this.ribs.defVal(ident.id, res);
}
// const text = this.ctx.identText(ident.id);
// this.ctx.report({
// severity: "error",
// file: this.currentFile,
// span: ident.span,
// msg: `redefinition of type '${text}'`,
// });
//
// const text = this.ctx.identText(ident.id);
// this.ctx.report({
// severity: "error",
// file: this.currentFile,
// span: ident.span,
// msg: `redefinition of value '${text}'`,
// });
}

24
compiler/ty/to_string.ts Normal file
View File

@ -0,0 +1,24 @@
import { Ctx } from "../ctx.ts";
import { exhausted } from "../util.ts";
import { Ty } from "./ty.ts";
export function tyToString(ctx: Ctx, ty: Ty): string {
const k = ty.kind;
switch (k.tag) {
case "error":
return `<error>`;
case "unknown":
return `<unknown>`;
case "null":
return `null`;
case "fn": {
const identText = ctx.identText(k.item.ident.id);
const params = k.params
.map((param) => tyToString(ctx, param))
.join(", ");
const reTy = tyToString(ctx, k.returnTy);
return `fn ${identText}(${params}) -> ${reTy}`;
}
}
exhausted(k);
}

19
compiler/ty/ty.ts Normal file
View File

@ -0,0 +1,19 @@
import * as ast from "../ast/mod.ts";
export type Ty = {
kind: TyKind;
};
export const Ty = (kind: TyKind): Ty => ({ kind });
export type TyKind =
| { tag: "error" }
| { tag: "unknown" }
| { tag: "null" }
| {
tag: "fn";
item: ast.Item;
kind: ast.FnItem;
params: Ty[];
returnTy: Ty;
};

View File

@ -3,7 +3,7 @@ export function todo<T>(msg?: string): T {
throw new NotImplemented(msg);
}
export function exhausted(_: never) {
export function exhausted<T>(_: never): T {
class Unexhausted extends Error {}
throw new Unexhausted();
}