├── .vscode └── settings.json ├── README.md ├── article ├── README.md ├── arith.ts ├── arith_test.ts ├── basic.ts ├── basic_test.ts └── utils.ts ├── book ├── LICENSE ├── tiny-ts-parser.ts └── typecheckers │ ├── arith.ts │ ├── arith_test.ts │ ├── basic.ts │ ├── basic2.ts │ ├── basic2_test.ts │ ├── basic_test.ts │ ├── obj.ts │ ├── obj_test.ts │ ├── poly.ts │ ├── poly_bug.ts │ ├── poly_test.ts │ ├── rec.ts │ ├── rec2.ts │ ├── rec2_test.ts │ ├── rec_test.ts │ ├── recfunc.ts │ ├── recfunc2.ts │ ├── recfunc2_test.ts │ ├── recfunc_test.ts │ ├── sub.ts │ ├── sub_test.ts │ ├── union.ts │ └── union_test.ts ├── deno.jsonc └── deno.lock /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.config": "deno.jsonc" 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 『型システムのしくみ』サポートサイト 2 | 3 | このリポジトリでは、遠藤侑介著 [『型システムのしくみ ー TypeScriptで実装しながら学ぶ型とプログラミング言語』](https://www.lambdanote.com/collections/type-systems) で利用しているライブラリなどを公開しています。 4 | 同書の基となった [『n月刊ラムダノート Vol.4, No.3』](https://www.lambdanote.com/collections/n-1/products/n-vol-4-no-3-2024) の記事「TypeScriptではじめる型システム」のコードも含まれています。 5 | 6 | いずれも使い方は各ディレクトリのREADMEおよび書籍または記事を参照してください。 7 | 8 | ## 書籍『型システムのしくみ』で利用するライブラリなど 9 | 10 | `book`ディレクトリ内には、パーサ用のライブラリと書籍の各章で実装する型検査器の例があります。 11 | 12 | - [book/tiny-ts-parser.ts](https://github.com/LambdaNote/support-ts-tapl/blob/main/book/tiny-ts-parser.ts) :対象言語の構文をパースするためのライブラリ(使い方は同書の1.4節を参照) 13 | - [book/typecheckers/](https://github.com/LambdaNote/support-ts-tapl/blob/main/book/typecheckers) :各章で実装する型検査器 14 | 15 | 実体は https://github.com/mame/tiny-ts-parser をsubtreeとして `book` に取り込んでいます。もし本リポジトリが更新されていない場合は [元リポジトリ](https://github.com/mame/tiny-ts-parser) をあたってみてください。 16 | 17 | ## 「TypeScriptではじめる型システム」で利用するライブラリなど 18 | 19 | `article`ディレクトリ内には、記事中に出てくる下記のファイルがあります。 20 | 21 | - [article/utils.ts](https://github.com/LambdaNote/support-ts-tapl/blob/main/article/utils.ts) :対象言語の構文をパースするためのライブラリ 22 | - [article/arith.ts](https://github.com/LambdaNote/support-ts-tapl/blob/main/article/arith.ts) :1.2節で作る型検査器 23 | - [article/basic.ts](https://github.com/LambdaNote/support-ts-tapl/blob/main/article/basic.ts) :1.3節と1.4節で作る型検査器 24 | 25 | ## 書籍の正誤情報 26 | 27 | 正誤情報は下記を参照してください。 28 | 29 | - [正誤情報](https://github.com/LambdaNote/errata-typesystems-1-1/issues?q=is%3Aissue+is%3Aopen+sort%3Acreated-asc) 30 | - [新しい正誤を報告する](https://github.com/LambdaNote/errata-typesystems-1-1)(新しいissueを立てるのではなく、ページごとに指定されているissueにコメントをしてください) 31 | 32 | -------------------------------------------------------------------------------- /article/README.md: -------------------------------------------------------------------------------- 1 | # 「TypeScriptではじめる型システム」サポートサイト 2 | 3 | - [utils.ts](https://github.com/LambdaNote/support-ts-tapl/blob/main/utils.ts) :対象言語の構文をパースするためのライブラリ 4 | - [arith.ts](https://github.com/LambdaNote/support-ts-tapl/blob/main/arith.ts) :1.2節で作る型検査器 5 | - [basic.ts](https://github.com/LambdaNote/support-ts-tapl/blob/main/basic.ts) :1.3節と1.4節で作る型検査器 6 | 7 | ## テスト方法 8 | 9 | ``` 10 | $ deno test -A 11 | ``` 12 | -------------------------------------------------------------------------------- /article/arith.ts: -------------------------------------------------------------------------------- 1 | import { error } from "./utils.ts"; 2 | 3 | type Type = 4 | | { tag: "Boolean" } 5 | | { tag: "Number" }; 6 | 7 | type Term = 8 | | { tag: "true" } 9 | | { tag: "false" } 10 | | { tag: "if"; cond: Term; thn: Term; els: Term } 11 | | { tag: "number"; n: number } 12 | | { tag: "add"; left: Term; right: Term }; 13 | 14 | export function typecheck(t: Term): Type { 15 | switch (t.tag) { 16 | case "true": 17 | return { tag: "Boolean" }; 18 | case "false": 19 | return { tag: "Boolean" }; 20 | case "if": { 21 | const condTy = typecheck(t.cond); 22 | if (condTy.tag !== "Boolean") error("boolean expected", t.cond); 23 | const thnTy = typecheck(t.thn); 24 | const elsTy = typecheck(t.els); 25 | if (thnTy.tag !== elsTy.tag) error("then and else have different types", t); 26 | return thnTy; 27 | } 28 | case "number": 29 | return { tag: "Number" }; 30 | case "add": { 31 | const leftTy = typecheck(t.left); 32 | if (leftTy.tag !== "Number") error("number expected", t.left); 33 | const rightTy = typecheck(t.right); 34 | if (rightTy.tag !== "Number") error("number expected", t.left); 35 | return { tag: "Number" }; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /article/arith_test.ts: -------------------------------------------------------------------------------- 1 | import { generateTestUtils, parseArith } from "./utils.ts"; 2 | import { typecheck } from "./arith.ts"; 3 | 4 | const { ok, ng } = generateTestUtils((code) => typecheck(parseArith(code))); 5 | 6 | Deno.test("true", () => ok("boolean", `true`)); 7 | Deno.test("false", () => ok("boolean", `false`)); 8 | Deno.test("if", () => ok("number", `true ? 1 : 2`)); 9 | Deno.test("if error", () => ng("test.ts:1:1-1:16 then and else have different types", `true ? 1 : true`)); 10 | 11 | Deno.test("number", () => ok("number", `1`)); 12 | Deno.test("add", () => ok("number", `1 + 2`)); 13 | Deno.test("add error", () => ng("test.ts:1:1-1:2 number expected", `1 + true`)); 14 | -------------------------------------------------------------------------------- /article/basic.ts: -------------------------------------------------------------------------------- 1 | import { error } from "./utils.ts"; 2 | 3 | type Type = 4 | | { tag: "Boolean" } 5 | | { tag: "Number" } 6 | | { tag: "Func"; params: Param[]; retType: Type }; 7 | 8 | type Param = { name: string; type: Type }; 9 | 10 | type Term = 11 | | { tag: "true" } 12 | | { tag: "false" } 13 | | { tag: "if"; cond: Term; thn: Term; els: Term } 14 | | { tag: "number"; n: number } 15 | | { tag: "add"; left: Term; right: Term } 16 | | { tag: "var"; name: string } 17 | | { tag: "func"; params: Param[]; body: Term } 18 | | { tag: "call"; func: Term; args: Term[] } 19 | | { tag: "seq"; body: Term; rest: Term } 20 | | { tag: "const"; name: string; init: Term; rest: Term }; 21 | 22 | type TypeEnv = Record; 23 | 24 | function typeEq(ty1: Type, ty2: Type): boolean { 25 | switch (ty2.tag) { 26 | case "Boolean": 27 | return ty1.tag === "Boolean"; 28 | case "Number": 29 | return ty1.tag === "Number"; 30 | case "Func": { 31 | if (ty1.tag !== "Func") return false; 32 | if (ty1.params.length !== ty2.params.length) return false; 33 | if ( 34 | !ty1.params.every( 35 | (param1, i) => typeEq(param1.type, ty2.params[i].type), 36 | ) 37 | ) return false; 38 | if (!typeEq(ty1.retType, ty2.retType)) return false; 39 | return true; 40 | } 41 | } 42 | } 43 | 44 | export function typecheck(t: Term, tyEnv: TypeEnv): Type { 45 | switch (t.tag) { 46 | case "true": 47 | return { tag: "Boolean" }; 48 | case "false": 49 | return { tag: "Boolean" }; 50 | case "if": { 51 | const condTy = typecheck(t.cond, tyEnv); 52 | if (condTy.tag !== "Boolean") error("boolean expected", t.cond); 53 | const thnTy = typecheck(t.thn, tyEnv); 54 | const elsTy = typecheck(t.els, tyEnv); 55 | if (!typeEq(thnTy, elsTy)) error("then and else have different types", t); 56 | return thnTy; 57 | } 58 | case "number": 59 | return { tag: "Number" }; 60 | case "add": { 61 | const leftTy = typecheck(t.left, tyEnv); 62 | if (leftTy.tag !== "Number") error("number expected", t.left); 63 | const rightTy = typecheck(t.right, tyEnv); 64 | if (rightTy.tag !== "Number") error("number expected", t.left); 65 | return { tag: "Number" }; 66 | } 67 | case "var": { 68 | if (!(t.name in tyEnv)) error(`unknown variable: ${t.name}`, t); 69 | return tyEnv[t.name]; 70 | } 71 | case "func": { 72 | const newTyEnv = t.params.reduce((tyEnv, { name, type }) => ({ ...tyEnv, [name]: type }), tyEnv); 73 | const retType = typecheck(t.body, newTyEnv); 74 | return { tag: "Func", params: t.params, retType }; 75 | } 76 | case "call": { 77 | const funcTy = typecheck(t.func, tyEnv); 78 | if (funcTy.tag !== "Func") error("function type expected", t.func); 79 | if (funcTy.params.length !== t.args.length) error("wrong number of arguments", t); 80 | t.args.forEach((arg, i) => { 81 | const argTy = typecheck(arg, tyEnv); 82 | if (!typeEq(argTy, funcTy.params[i].type)) error("parameter type mismatch", arg); 83 | }); 84 | return funcTy.retType; 85 | } 86 | case "seq": 87 | typecheck(t.body, tyEnv); 88 | return typecheck(t.rest, tyEnv); 89 | case "const": { 90 | const ty = typecheck(t.init, tyEnv); 91 | const newTyEnv = { ...tyEnv, [t.name]: ty }; 92 | return typecheck(t.rest, newTyEnv); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /article/basic_test.ts: -------------------------------------------------------------------------------- 1 | import { generateTestUtils, parseBasic } from "./utils.ts"; 2 | import { typecheck } from "./basic.ts"; 3 | 4 | const { ok, ng } = generateTestUtils((code) => typecheck(parseBasic(code), {})); 5 | 6 | Deno.test("func", () => ok("(x: number, y: number) => boolean", `(x: number, y: Number) => true`)); 7 | Deno.test("call", () => ok("boolean", `( (x: number, y: number) => true )(1, 2)`)); 8 | Deno.test("call error", () => 9 | ng("test.ts:1:40-1:41 parameter type mismatch", `( (x: number, y: boolean) => true )(1, 2)`)); 10 | Deno.test("var", () => ok("number", `((x: number, y: number) => x)(1, 2)`)); 11 | Deno.test("var error", () => ng("test.ts:1:28-1:29 unknown variable: z", `((x: number, y: number) => z)(1, 2)`)); 12 | Deno.test("func func", () => ok("(f: (x: number) => number) => number", `( (f: (x: number) => number) => f(42) )`)); 13 | 14 | Deno.test("seq 1", () => ok("number", `1; 2; 3`)); 15 | Deno.test("seq 2", () => ok("boolean", `1; 2; true`)); 16 | Deno.test("const 1", () => 17 | ok( 18 | "number", 19 | ` 20 | const x = 1; 21 | const y = 2; 22 | x + y; 23 | `, 24 | )); 25 | 26 | Deno.test("const 2", () => 27 | ok( 28 | "number", 29 | ` 30 | const f = (x: number, y: number) => x + y; 31 | f(1, 2); 32 | `, 33 | )); 34 | Deno.test("const error 1", () => 35 | ng( 36 | "", 37 | ` 38 | const f = (x: number, y: number) => x + y; 39 | f(1, true); 40 | `, 41 | )); 42 | Deno.test("const error 2", () => 43 | ng( 44 | "", 45 | ` 46 | const fib = (n: number) => { 47 | return fib(n + 1) + fib(n); 48 | }; 49 | fib(1) 50 | `, 51 | )); 52 | -------------------------------------------------------------------------------- /article/utils.ts: -------------------------------------------------------------------------------- 1 | import p from "npm:@typescript-eslint/typescript-estree@7.11.0"; 2 | import { assertEquals, assertThrows } from "https://deno.land/std@0.224.0/assert/mod.ts"; 3 | 4 | type Position = { line: number; column: number }; 5 | type Location = { start: Position; end: Position }; 6 | 7 | export type Type = 8 | | { loc?: Location; tag: "Boolean" } // boolean 9 | | { loc?: Location; tag: "Number" } // number 10 | | { loc?: Location; tag: "Func"; params: Param[]; retType: Type } // (_: T0, ...) => T 11 | | { loc?: Location; tag: "Object"; props: PropertyType[] } // { s0: T0, ... } 12 | | { loc?: Location; tag: "TaggedUnion"; variants: VariantType[] } // { tag: "s0", val: T } | ... 13 | | { loc?: Location; tag: "Rec"; name: string; type: Type } // mu V. T 14 | | { loc?: Location; tag: "Forall"; typeParams: string[]; type: Type } // T 15 | | { loc?: Location; tag: "TypeVar"; name: string; typeArgs?: Type[] } // V 16 | ; 17 | 18 | type Param = { name: string; type: Type }; 19 | type PropertyType = { name: string; type: Type }; 20 | type VariantType = { label: string; type: Type }; 21 | 22 | type Term = 23 | | { loc: Location; tag: "true" } 24 | | { loc: Location; tag: "false" } 25 | | { loc: Location; tag: "if"; cond: Term; thn: Term; els: Term } 26 | | { loc: Location; tag: "number"; n: number } 27 | | { loc: Location; tag: "add"; left: Term; right: Term } 28 | | { loc: Location; tag: "var"; name: string } 29 | | { loc: Location; tag: "func"; params: Param[]; body: Term } 30 | | { loc: Location; tag: "call"; func: Term; args: Term[] } 31 | | { loc: Location; tag: "seq"; body: Term; rest: Term } 32 | | { loc: Location; tag: "const"; name: string; init: Term; rest: Term } 33 | | { loc: Location; tag: "objectNew"; props: PropertyTerm[] } 34 | | { loc: Location; tag: "objectGet"; obj: Term; propName: string } 35 | | { loc: Location; tag: "taggedUnionNew"; label: string; term: Term; as: Type } 36 | | { loc: Location; tag: "taggedUnionGet"; varName: string; clauses: VariantTerm[] } 37 | | { loc: Location; tag: "recFunc"; funcName: string; params: Param[]; retType: Type; body: Term; rest: Term } 38 | | { loc: Location; tag: "typeAbs"; typeParams: string[]; body: Term } 39 | | { loc: Location; tag: "typeApp"; typeAbs: Term; typeArgs: Type[] }; 40 | 41 | type PropertyTerm = { name: string; term: Term }; 42 | type VariantTerm = { label: string; term: Term }; 43 | 44 | type TypeAliasMap = Record; 45 | type TypeVarBindings = Record; 46 | type Context = { 47 | globalTypeAliasMap: TypeAliasMap; 48 | typeVarBindings: TypeVarBindings; 49 | }; 50 | 51 | export function typeShow(ty: Type): string { 52 | switch (ty.tag) { 53 | case "Boolean": 54 | return "boolean"; 55 | case "Number": 56 | return "number"; 57 | case "Func": { 58 | const params = ty.params.map(({ name, type }) => `${name}: ${typeShow(type)}`); 59 | return `(${params.join(", ")}) => ${typeShow(ty.retType)}`; 60 | } 61 | case "Object": { 62 | const props = ty.props.map(({ name, type }) => `${name}: ${typeShow(type)}`); 63 | return `{ ${props.join(", ")} }`; 64 | } 65 | case "TaggedUnion": { 66 | const variants = ty.variants.map(({ label, type }) => `{ tag: "${label}", val: ${typeShow(type)} }`); 67 | return `(${variants.join(" | ")})`; 68 | } 69 | case "Rec": 70 | return `(mu ${ty.name}. ${typeShow(ty.type)})`; 71 | case "Forall": 72 | return `<${ty.typeParams.join(", ")}>${typeShow(ty.type)}`; 73 | case "TypeVar": 74 | return ty.name; 75 | } 76 | } 77 | 78 | function freeTyVars(ty: Type): Set { 79 | switch (ty.tag) { 80 | case "Boolean": 81 | case "Number": 82 | return new Set(); 83 | case "Func": 84 | return ty.params.reduce((r, { type }) => r.union(freeTyVars(type)), freeTyVars(ty.retType)); 85 | case "Object": 86 | return ty.props.reduce((r, { type }) => r.union(freeTyVars(type)), new Set()); 87 | case "TaggedUnion": 88 | return ty.variants.reduce((r, { type }) => r.union(freeTyVars(type)), new Set()); 89 | case "Rec": 90 | return freeTyVars(ty.type).difference(new Set([ty.name])); 91 | case "Forall": 92 | return freeTyVars(ty.type).difference(new Set(ty.typeParams)); 93 | case "TypeVar": 94 | return new Set([ty.name]); 95 | } 96 | } 97 | 98 | function addDefinedTypeVars(tyVars: string[], ctx: Context): Context { 99 | return { 100 | globalTypeAliasMap: ctx.globalTypeAliasMap, 101 | typeVarBindings: tyVars.reduce( 102 | (bindings, name) => ({ ...bindings, [name]: { tag: "TypeVar", name } }), 103 | ctx.typeVarBindings, 104 | ), 105 | }; 106 | } 107 | 108 | function expandTypeAliases(ty: Type, recDefined: Set, ctx: Context): Type { 109 | switch (ty.tag) { 110 | case "Boolean": 111 | return { tag: "Boolean" }; 112 | case "Number": 113 | return { tag: "Number" }; 114 | case "Func": { 115 | const params = ty.params.map(({ name, type }) => ({ name, type: expandTypeAliases(type, recDefined, ctx) })); 116 | const retType = expandTypeAliases(ty.retType, recDefined, ctx); 117 | return { tag: "Func", params, retType }; 118 | } 119 | case "Object": { 120 | const props = ty.props.map(({ name, type }) => ({ name, type: expandTypeAliases(type, recDefined, ctx) })); 121 | return { tag: "Object", props }; 122 | } 123 | case "TaggedUnion": { 124 | const variants = ty.variants.map(({ label, type }) => ({ 125 | label, 126 | type: expandTypeAliases(type, recDefined, ctx), 127 | })); 128 | return { tag: "TaggedUnion", variants }; 129 | } 130 | case "Rec": { 131 | const newCtx = addDefinedTypeVars([ty.name], ctx); 132 | return { tag: "Rec", name: ty.name, type: expandTypeAliases(ty.type, recDefined, newCtx) }; 133 | } 134 | case "Forall": { 135 | const newCtx = addDefinedTypeVars(ty.typeParams, ctx); 136 | return { tag: "Forall", typeParams: ty.typeParams, type: expandTypeAliases(ty.type, recDefined, newCtx) }; 137 | } 138 | case "TypeVar": { 139 | const typeArgs = ty.typeArgs; 140 | if (typeArgs) { 141 | if (ty.name in ctx.typeVarBindings) error(`not a generic type: ${ty.name}`, ty); 142 | if (recDefined.has(ty.name)) error(`type recursion for generics is not supported`, ty); 143 | if (!(ty.name in ctx.globalTypeAliasMap)) error(`unbound type variable: ${ty.name}`, ty); 144 | const { typeParams, type } = ctx.globalTypeAliasMap[ty.name]; 145 | if (!typeParams) error(`not a generic type: ${ty.name}`, ty); 146 | if (typeParams.length !== typeArgs.length) error(`wrong number of type arguments for ${ty.name}`, ty); 147 | const typeVarBindings = typeParams.reduce((bindings, typeParam, i) => { 148 | return { ...bindings, [typeParam]: expandTypeAliases(typeArgs[i], recDefined, ctx) }; 149 | }, ctx.typeVarBindings); 150 | const newCtx: Context = { 151 | globalTypeAliasMap: ctx.globalTypeAliasMap, 152 | typeVarBindings, 153 | }; 154 | const retTy = expandTypeAliases(type, recDefined.union(new Set([ty.name])), newCtx); 155 | if (freeTyVars(retTy).has(ty.name)) error("bug?", ty); 156 | return retTy; 157 | } else { 158 | if (ty.name in ctx.typeVarBindings) return ctx.typeVarBindings[ty.name]; 159 | if (recDefined.has(ty.name)) return { tag: "TypeVar", name: ty.name }; 160 | if (!(ty.name in ctx.globalTypeAliasMap)) error(`unbound type variable: ${ty.name}`, ty); 161 | const { typeParams, type } = ctx.globalTypeAliasMap[ty.name]; 162 | if (typeParams) error(`type arguments are required for ${ty.name}`, ty); 163 | const newCtx: Context = { 164 | globalTypeAliasMap: ctx.globalTypeAliasMap, 165 | typeVarBindings: {}, 166 | }; 167 | const retTy = expandTypeAliases(type, recDefined.union(new Set([ty.name])), newCtx); 168 | return freeTyVars(retTy).has(ty.name) ? { tag: "Rec", name: ty.name, type: retTy } : retTy; 169 | } 170 | } 171 | } 172 | } 173 | 174 | function simplifyType(ty: Type, ctx: Context) { 175 | return expandTypeAliases(ty, new Set(), ctx); 176 | } 177 | 178 | function getIdentifier(node: p.TSESTree.Expression | p.TSESTree.PrivateIdentifier | p.TSESTree.TSQualifiedName) { 179 | if (node.type !== "Identifier") error("identifier expected", node); 180 | return node.name; 181 | } 182 | 183 | function getLiteralString(node: p.TSESTree.Expression) { 184 | if (node.type !== "Literal" || typeof node.value !== "string") error("", node); 185 | return node.value; 186 | } 187 | 188 | function getTypeProp(member: p.TSESTree.TypeElement) { 189 | if (member.type !== "TSPropertySignature" || member.key.type !== "Identifier" || !member.typeAnnotation) { 190 | error("object type must have only normal key-value paris", member); 191 | } 192 | return { key: member.key.name, type: member.typeAnnotation.typeAnnotation }; 193 | } 194 | 195 | function getProp(property: p.TSESTree.ObjectLiteralElement) { 196 | if (property.type === "SpreadElement") error("spread operator is not allowed", property); 197 | if (property.computed) error("key must be bare", property); 198 | if (property.value.type === "AssignmentPattern") error("AssignmentPattern is not allowed", property); 199 | if (property.value.type === "TSEmptyBodyFunctionExpression") { 200 | error("TSEmptyBodyFunctionExpression is not allowed", property); 201 | } 202 | return { key: getIdentifier(property.key), value: property.value }; 203 | } 204 | 205 | function getTagAndVal(node: p.TSESTree.ObjectExpression) { 206 | if (node.properties.length !== 2) error(`tagged union must be { tag: "TAG", val: EXPR }`, node); 207 | const { key: s0, value: v0 } = getProp(node.properties[0]); 208 | const { key: s1, value: v1 } = getProp(node.properties[1]); 209 | if (s0 !== "tag" || s1 !== "val") error(`tagged union must be { tag: "TAG", val: EXPR }`, node); 210 | 211 | return { tag: getLiteralString(v0), val: v1 }; 212 | } 213 | 214 | function getSwitchVarName(node: p.TSESTree.Expression) { 215 | if (node.type !== "MemberExpression" || node.computed || getIdentifier(node.property) !== "tag") { 216 | error(`switch statement must be switch(VAR.tag) { ... }`, node); 217 | } 218 | return getIdentifier(node.object); 219 | } 220 | 221 | function getParam(node: p.TSESTree.Parameter): Param { 222 | if (node.type !== "Identifier") error("parameter must be a variable", node); 223 | if (!node.typeAnnotation) error("parameter type is required", node); 224 | const name = node.name; 225 | const type = convertType(node.typeAnnotation.typeAnnotation); 226 | return { name, type }; 227 | } 228 | 229 | function getRecFunc(node: p.TSESTree.FunctionDeclaration, ctx: Context, restFunc: (() => Term) | null): Term { 230 | if (!node.id) error("function name is required", node); 231 | if (!node.returnType) error("function return type is required", node); 232 | const funcName = node.id.name; 233 | if (node.typeParameters) error("type parameter is not supported for function declaration", node); 234 | const params = node.params.map((param) => { 235 | const { name, type } = getParam(param); 236 | return { name, type: simplifyType(type, ctx) }; 237 | }); 238 | const retType = simplifyType(convertType(node.returnType.typeAnnotation), ctx); 239 | const body = convertStmts(node.body.body, true, ctx); 240 | const rest: Term = restFunc ? restFunc() : { tag: "var", name: funcName, loc: node.loc }; 241 | return { tag: "recFunc", funcName, params, retType, body, rest, loc: node.loc }; 242 | } 243 | 244 | function convertType(node: p.TSESTree.TypeNode): Type { 245 | switch (node.type) { 246 | case "TSBooleanKeyword": 247 | return { tag: "Boolean", loc: node.loc }; 248 | case "TSNumberKeyword": 249 | return { tag: "Number", loc: node.loc }; 250 | case "TSFunctionType": { 251 | if (!node.returnType) error("return type is required", node); 252 | const params = node.params.map((param) => getParam(param)); 253 | const retType = convertType(node.returnType.typeAnnotation); 254 | const funcTy: Type = { tag: "Func", params, retType }; 255 | if (!node.typeParameters) return funcTy; 256 | const typeParams = node.typeParameters.params.map((typeParameter) => typeParameter.name.name); 257 | return { tag: "Forall", typeParams, type: funcTy, loc: node.loc }; 258 | } 259 | case "TSTypeLiteral": { 260 | const props = node.members.map((member) => { 261 | const { key, type } = getTypeProp(member); 262 | return { name: key, type: convertType(type) }; 263 | }); 264 | return { tag: "Object", props, loc: node.loc }; 265 | } 266 | case "TSUnionType": { 267 | const variants = node.types.map((variant) => { 268 | if (variant.type !== "TSTypeLiteral" || variant.members.length !== 2) { 269 | error(`tagged union type must have { tag: "TAG", val: TYPE }`, variant); 270 | } 271 | const { key: s0, type: ty0 } = getTypeProp(variant.members[0]); 272 | const { key: s1, type: ty1 } = getTypeProp(variant.members[1]); 273 | if (s0 !== "tag" || ty0.type !== "TSLiteralType" || s1 !== "val") { 274 | error(`tagged union type must have { tag: "TAG", val: TYPE }`, variant); 275 | } 276 | const label = getLiteralString(ty0.literal); 277 | const type = convertType(ty1); 278 | return { label, type }; 279 | }); 280 | return { tag: "TaggedUnion", variants, loc: node.loc }; 281 | } 282 | case "TSTypeReference": { 283 | const typeArgs = node.typeArguments?.params.map((tyArg) => convertType(tyArg)); 284 | const name = getIdentifier(node.typeName); 285 | return { tag: "TypeVar", name, typeArgs, loc: node.loc }; 286 | } 287 | default: 288 | error(`unknown node: ${node.type}`, node); 289 | } 290 | } 291 | 292 | function convertExpr(node: p.TSESTree.Expression, ctx: Context): Term { 293 | switch (node.type) { 294 | case "BinaryExpression": { 295 | if (node.operator !== "+") error(`unsupported operator: ${node.operator}`, node); 296 | if (node.left.type === "PrivateIdentifier") error("private identifer is not allowed", node.left); 297 | const left = convertExpr(node.left, ctx); 298 | const right = convertExpr(node.right, ctx); 299 | return { tag: "add", left, right, loc: node.loc }; 300 | } 301 | case "Identifier": 302 | return { tag: "var", name: node.name, loc: node.loc }; 303 | // deno-lint-ignore no-fallthrough 304 | case "Literal": 305 | switch (typeof node.value) { 306 | case "number": 307 | return { tag: "number", n: node.value, loc: node.loc }; 308 | case "boolean": 309 | return { tag: node.value ? "true" : "false", loc: node.loc }; 310 | default: 311 | error(`unsupported literal: ${node.value}`, node); 312 | } 313 | case "ArrowFunctionExpression": { 314 | const typeParams = node.typeParameters?.params.map((typeParameter) => typeParameter.name.name); 315 | const newCtx = typeParams ? addDefinedTypeVars(typeParams, ctx) : ctx; 316 | const params = node.params.map((param) => { 317 | const { name, type } = getParam(param); 318 | return { name, type: simplifyType(type, newCtx) }; 319 | }); 320 | if (node.returnType) error("return type is not required for arrow function", node.returnType); 321 | const body = node.body.type === "BlockStatement" 322 | ? convertStmts(node.body.body, true, newCtx) 323 | : convertExpr(node.body, newCtx); 324 | const func: Term = { tag: "func", params, body, loc: node.loc }; 325 | return typeParams ? { tag: "typeAbs", typeParams, body: func, loc: node.typeParameters!.loc } : func; 326 | } 327 | case "CallExpression": { 328 | const args = node.arguments.map((argument) => { 329 | if (argument.type === "SpreadElement") error("argument must be an expression", argument); 330 | return convertExpr(argument, ctx); 331 | }); 332 | let func = convertExpr(node.callee, ctx); 333 | if (node.typeArguments) { 334 | const typeArgs = node.typeArguments.params.map((param) => simplifyType(convertType(param), ctx)); 335 | func = { tag: "typeApp", typeAbs: func, typeArgs, loc: node.loc }; 336 | } 337 | return { tag: "call", func, args, loc: node.loc }; 338 | } 339 | case "TSAsExpression": 340 | case "TSTypeAssertion": { 341 | if (node.expression.type !== "ObjectExpression") { 342 | error(`type assertion must be { tag: "TAG", val: EXPR }`, node); 343 | } 344 | const ty = simplifyType(convertType(node.typeAnnotation), ctx); 345 | const { tag, val } = getTagAndVal(node.expression); 346 | const term = convertExpr(val, ctx); 347 | return { tag: "taggedUnionNew", label: tag, term, as: ty, loc: node.loc }; 348 | } 349 | case "MemberExpression": { 350 | if (node.computed || node.property.type !== "Identifier") error("object member must be OBJ.STR", node.property); 351 | const obj = convertExpr(node.object, ctx); 352 | const propName = node.property.name; 353 | return { tag: "objectGet", obj, propName, loc: node.loc }; 354 | } 355 | case "ObjectExpression": { 356 | const props = node.properties.map((property) => { 357 | const { key: name, value } = getProp(property); 358 | return { name, term: convertExpr(value, ctx) }; 359 | }); 360 | return { tag: "objectNew", props, loc: node.loc }; 361 | } 362 | case "ConditionalExpression": { 363 | const cond = convertExpr(node.test, ctx); 364 | const thn = convertExpr(node.consequent, ctx); 365 | if (!node.alternate) error("else clause is requried", node); 366 | const els = convertExpr(node.alternate, ctx); 367 | return { tag: "if", cond, thn, els, loc: node.loc }; 368 | } 369 | case "TSNonNullExpression": 370 | return convertExpr(node.expression, ctx); 371 | case "TSInstantiationExpression": { 372 | const typeArgs = node.typeArguments.params.map((param) => simplifyType(convertType(param), ctx)); 373 | const typeAbs = convertExpr(node.expression, ctx); 374 | return { tag: "typeApp", typeAbs, typeArgs, loc: node.loc }; 375 | } 376 | default: 377 | error(`unsupported expression node: ${node.type}`, node); 378 | } 379 | } 380 | 381 | function convertStmts(nodes: p.TSESTree.Statement[], requireReturn: boolean, ctx: Context): Term { 382 | function convertStmt(i: number, ctx: Context): Term { 383 | const last = nodes.length - 1 === i; 384 | const node = nodes[i]; 385 | switch (node.type) { 386 | case "VariableDeclaration": { 387 | if (last && requireReturn) error("return is required", node); 388 | if (node.declarations.length !== 1) error("multiple variable declaration is not allowed", node); 389 | const decl = node.declarations[0]; 390 | if (!decl.init) error("variable initializer is required", decl); 391 | const name = getIdentifier(decl.id); 392 | const init = convertExpr(decl.init, ctx); 393 | const rest: Term = last ? { tag: "var", name, loc: node.loc } : convertStmt(i + 1, ctx); 394 | return { tag: "const", name, init, rest, loc: node.loc }; 395 | } 396 | case "ExportNamedDeclaration": { 397 | if (last && requireReturn) error("return is required", node); 398 | if (!node.declaration || node.declaration.type !== "FunctionDeclaration") { 399 | error("export must have a function declaration", node); 400 | } 401 | return getRecFunc(node.declaration, ctx, last ? null : () => convertStmt(i + 1, ctx)); 402 | } 403 | case "FunctionDeclaration": { 404 | if (last && requireReturn) error("return is required", node); 405 | return getRecFunc(node, ctx, last ? null : () => convertStmt(i + 1, ctx)); 406 | } 407 | case "ExpressionStatement": { 408 | if (last && requireReturn) error("return is required", node); 409 | const body = convertExpr(node.expression, ctx); 410 | return last ? body : { tag: "seq", body, rest: convertStmt(i + 1, ctx), loc: node.loc }; 411 | } 412 | case "ReturnStatement": { 413 | if (!node.argument) error("return must have an argument", node); 414 | return convertExpr(node.argument, ctx); 415 | } 416 | case "IfStatement": { 417 | const cond = convertExpr(node.test, ctx); 418 | const thn = convertStmts([node.consequent], requireReturn, ctx); 419 | if (!node.alternate) error("else clause is requried", node); 420 | const els = convertStmts([node.alternate], requireReturn, ctx); 421 | return { tag: "if", cond, thn, els, loc: node.loc }; 422 | } 423 | case "SwitchStatement": { 424 | const varName = getSwitchVarName(node.discriminant); 425 | const clauses: VariantTerm[] = []; 426 | node.cases.forEach((caseNode) => { 427 | if (!caseNode.test) error("default case is not allowed", caseNode); 428 | const conseq = caseNode.consequent; 429 | const stmts = conseq.length === 1 && conseq[0].type === "BlockStatement" ? conseq[0].body : conseq; 430 | const clause = convertStmts(stmts, requireReturn, ctx); 431 | clauses.push({ label: getLiteralString(caseNode.test), term: clause }); 432 | }); 433 | return { tag: "taggedUnionGet", varName, clauses, loc: node.loc }; 434 | } 435 | default: 436 | error(`unsupported statement node: ${node.type}`, node); 437 | } 438 | } 439 | return convertStmt(0, ctx); 440 | } 441 | 442 | function convertProgram(nodes: p.TSESTree.Statement[]): Term { 443 | const globalTypeAliasMap: TypeAliasMap = { 444 | "Boolean": { typeParams: null, type: { tag: "Boolean" } }, 445 | "Number": { typeParams: null, type: { tag: "Number" } }, 446 | }; 447 | 448 | const stmts = nodes.filter((node) => { 449 | switch (node.type) { 450 | case "ImportDeclaration": 451 | return false; 452 | case "TSTypeAliasDeclaration": { 453 | const name = node.id.name; 454 | const typeParams = node.typeParameters ? node.typeParameters.params.map((param) => param.name.name) : null; 455 | const type = convertType(node.typeAnnotation); 456 | globalTypeAliasMap[name] = { typeParams, type }; 457 | return false; 458 | } 459 | default: 460 | return true; 461 | } 462 | }); 463 | 464 | const ctx = { globalTypeAliasMap, typeVarBindings: {} }; 465 | 466 | return convertStmts(stmts, false, ctx); 467 | } 468 | 469 | type ExtractTagsSub = T extends Type | Term ? ExtractTags 470 | : T extends Type[] ? ExtractTags[] 471 | : T extends Term[] ? ExtractTags[] 472 | : T extends (infer U)[] ? { [P in keyof U]: ExtractTags }[] 473 | : T; 474 | type ExtractTags = Extract<{ [P in keyof T]: ExtractTagsSub }, { tag: K }>; 475 | 476 | function subsetSystem( 477 | node: Term, 478 | keepTypes: Types[], 479 | keepTerms: Terms[], 480 | ): ExtractTags { 481 | if (!node.tag) return node; 482 | 483 | if (!( keepTypes).includes(node.tag) && !( keepTerms).includes(node.tag)) { 484 | throw new Error(`"${node.tag}" is not allowed in this system`); 485 | } 486 | 487 | // deno-lint-ignore no-explicit-any 488 | const newNode: any = {}; 489 | Object.entries(node).forEach(([key, val]) => { 490 | if (typeof val !== "object" || !val.tag) { 491 | newNode[key] = val; 492 | } else { 493 | newNode[key] = subsetSystem(val, keepTypes, keepTerms); 494 | } 495 | }); 496 | return newNode; 497 | } 498 | 499 | export function parse(code: string): Term { 500 | const node = p.parse(code, { allowInvalidAST: false, loc: true }); 501 | return convertProgram(node.body); 502 | } 503 | 504 | export function parseArith(code: string) { 505 | return subsetSystem( 506 | parse(code), 507 | ["Boolean", "Number"], 508 | ["true", "false", "if", "number", "add"], 509 | ); 510 | } 511 | 512 | export function parseBasic(code: string) { 513 | return subsetSystem( 514 | parse(code), 515 | ["Boolean", "Number", "Func"], 516 | ["true", "false", "if", "number", "add", "var", "func", "call", "seq", "const"], 517 | ); 518 | } 519 | 520 | export function parseExtended(code: string) { 521 | return subsetSystem( 522 | parse(code), 523 | ["Boolean", "Number", "Func", "Object", "TaggedUnion"], 524 | [ 525 | "true", 526 | "false", 527 | "if", 528 | "number", 529 | "add", 530 | "var", 531 | "func", 532 | "call", 533 | "seq", 534 | "const", 535 | "objectNew", 536 | "objectGet", 537 | "taggedUnionNew", 538 | "taggedUnionGet", 539 | "recFunc", 540 | ], 541 | ); 542 | } 543 | 544 | export function parseSub(code: string) { 545 | return subsetSystem( 546 | parse(code), 547 | ["Boolean", "Number", "Func", "Object"], 548 | ["true", "false", "number", "add", "var", "func", "call", "seq", "const", "objectNew", "objectGet"], 549 | ); 550 | } 551 | 552 | export function parseRec(code: string) { 553 | return subsetSystem( 554 | parse(code), 555 | ["Boolean", "Number", "Func", "Rec", "TypeVar"], 556 | ["true", "false", "if", "number", "add", "var", "func", "call", "seq", "const"], 557 | ); 558 | } 559 | 560 | export function parseRec2(code: string) { 561 | return subsetSystem( 562 | parse(code), 563 | ["Boolean", "Number", "Func", "Object", "TaggedUnion", "Rec", "TypeVar"], 564 | [ 565 | "true", 566 | "false", 567 | "if", 568 | "number", 569 | "add", 570 | "var", 571 | "func", 572 | "call", 573 | "seq", 574 | "const", 575 | "objectNew", 576 | "objectGet", 577 | "taggedUnionNew", 578 | "taggedUnionGet", 579 | "recFunc", 580 | ], 581 | ); 582 | } 583 | 584 | export function parsePoly(code: string) { 585 | return subsetSystem( 586 | parse(code), 587 | ["Boolean", "Number", "Func", "Forall", "TypeVar"], 588 | ["true", "false", "if", "number", "add", "var", "func", "call", "seq", "const", "typeAbs", "typeApp"], 589 | ); 590 | } 591 | 592 | // deno-lint-ignore no-explicit-any 593 | export function error(msg: string, node: any): never { 594 | if (node.loc) { 595 | const { start, end } = node.loc; 596 | throw new Error(`test.ts:${start.line}:${start.column + 1}-${end.line}:${end.column + 1} ${msg}`); 597 | } else { 598 | throw new Error(msg); 599 | } 600 | } 601 | 602 | // deno-lint-ignore no-explicit-any 603 | export function generateTestUtils(typecheck: (code: any) => Type) { 604 | return { 605 | ok: (expected: string, code: string) => { 606 | assertEquals(expected, typeShow(typecheck(code))); 607 | }, 608 | ng: (expected: string, code: string) => { 609 | assertThrows(() => typecheck(code), Error, expected); 610 | }, 611 | }; 612 | } 613 | -------------------------------------------------------------------------------- /book/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Yusuke Endoh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /book/tiny-ts-parser.ts: -------------------------------------------------------------------------------- 1 | import * as p from "npm:@typescript-eslint/typescript-estree@8.21.0"; 2 | 3 | type Position = { line: number; column: number }; 4 | type Location = { start: Position; end: Position }; 5 | 6 | // Integrated node definition of types and terms. 7 | // Not designed for type check but used to get a subset for each system. 8 | export type Type = 9 | | { loc?: Location; tag: "Boolean" } // boolean 10 | | { loc?: Location; tag: "Number" } // number 11 | | { loc?: Location; tag: "String" } // string 12 | | { loc?: Location; tag: "Func"; params: Param[]; retType: Type } // (_: T0, ...) => T 13 | | { loc?: Location; tag: "Option"; elemType: Type } // elemType | undefined 14 | | { loc?: Location; tag: "Array"; elemType: Type } // elemType[] 15 | | { loc?: Location; tag: "Record"; elemType: Type } // Record 16 | | { loc?: Location; tag: "Object"; props: PropertyType[] } // { s0: T0, ... } 17 | | { loc?: Location; tag: "TaggedUnion"; variants: VariantType[] } // { tag: "s0", val: T } | ... 18 | | { loc?: Location; tag: "Rec"; name: string; type: Type } // mu X. T 19 | | { loc?: Location; tag: "TypeAbs"; typeParams: string[]; type: Type } // T 20 | | { loc?: Location; tag: "TypeVar"; name: string; typeArgs?: Type[] } 21 | | { loc?: Location; tag: "Undefined" }; // X 22 | 23 | type Param = { name: string; type: Type }; 24 | type PropertyType = { name: string; type: Type }; 25 | type VariantType = { tagLabel: string; props: PropertyType[] }; 26 | 27 | type Term = 28 | | { loc: Location; tag: "true" } 29 | | { loc: Location; tag: "false" } 30 | | { loc: Location; tag: "not"; cond: Term } 31 | | { loc: Location; tag: "compare"; op: string; left: Term; right: Term } 32 | | { loc: Location; tag: "if"; cond: Term; thn: Term; els: Term } 33 | | { loc: Location; tag: "number"; n: number } 34 | | { loc: Location; tag: "add"; left: Term; right: Term } 35 | | { loc: Location; tag: "string"; s: string } 36 | | { loc: Location; tag: "var"; name: string } 37 | | { loc: Location; tag: "func"; params: Param[]; retType?: Type; body: Term } 38 | | { loc: Location; tag: "call"; func: Term; args: Term[] } 39 | | { loc: Location; tag: "seq"; body: Term; rest: Term } 40 | | { loc: Location; tag: "const"; name: string; init: Term; rest: Term } 41 | | { loc: Location; tag: "seq2"; body: Term[] } 42 | | { loc: Location; tag: "const2"; name: string; init: Term } 43 | | { loc: Location; tag: "assign"; name: string; init: Term } 44 | | { loc: Location; tag: "for"; ary: Term; body: Term; idx: string; rest: Term } 45 | | { loc: Location; tag: "forOf"; ary: Term; body: Term; var: string; rest: Term } 46 | | { loc: Location; tag: "arrayNew"; aryType: Type } 47 | | { loc: Location; tag: "arrayExt"; ary: Term; val: Term } 48 | | { loc: Location; tag: "recordNew"; recordType: Type } 49 | | { loc: Location; tag: "recordCopy"; record: Term } 50 | | { loc: Location; tag: "recordExt"; record: Term; key: Term; val: Term } 51 | | { loc: Location; tag: "recordIn"; record: Term; key: Term } 52 | | { loc: Location; tag: "member"; base: Term; index: Term } 53 | | { loc: Location; tag: "objectNew"; props: PropertyTerm[] } 54 | | { loc: Location; tag: "objectGet"; obj: Term; propName: string } 55 | | { loc: Location; tag: "taggedUnionNew"; tagLabel: string; props: PropertyTerm[]; as: Type } 56 | | { loc: Location; tag: "taggedUnionExt"; term: Term; as: Type } 57 | | { loc: Location; tag: "taggedUnionGet"; varName: string; clauses: VariantTerm[]; defaultClause: Term } 58 | | { loc: Location; tag: "recFunc"; funcName: string; params: Param[]; retType: Type; body: Term; rest: Term } 59 | | { loc: Location; tag: "typeAbs"; typeParams: string[]; body: Term } 60 | | { loc: Location; tag: "typeApp"; typeAbs: Term; typeArgs: Type[] } 61 | | { loc: Location; tag: "undefined" }; 62 | 63 | type PropertyTerm = { name: string; term: Term }; 64 | type VariantTerm = { tagLabel: string; term: Term }; 65 | 66 | // --- 67 | // Types and terms for each system (automatically generated): 68 | export type TypeForArith = 69 | | { tag: "Boolean" } 70 | | { tag: "Number" }; 71 | 72 | export type TermForArith = 73 | | { tag: "true" } 74 | | { tag: "false" } 75 | | { tag: "if"; cond: TermForArith; thn: TermForArith; els: TermForArith } 76 | | { tag: "number"; n: number } 77 | | { tag: "add"; left: TermForArith; right: TermForArith }; 78 | 79 | export type TypeForBasic = 80 | | { tag: "Boolean" } 81 | | { tag: "Number" } 82 | | { tag: "Func"; params: ParamForBasic[]; retType: TypeForBasic }; 83 | 84 | export type ParamForBasic = { name: string; type: TypeForBasic }; 85 | 86 | export type TermForBasic = 87 | | { tag: "true" } 88 | | { tag: "false" } 89 | | { tag: "if"; cond: TermForBasic; thn: TermForBasic; els: TermForBasic } 90 | | { tag: "number"; n: number } 91 | | { tag: "add"; left: TermForBasic; right: TermForBasic } 92 | | { tag: "var"; name: string } 93 | | { tag: "func"; params: ParamForBasic[]; body: TermForBasic } 94 | | { tag: "call"; func: TermForBasic; args: TermForBasic[] } 95 | | { tag: "seq"; body: TermForBasic; rest: TermForBasic } 96 | | { tag: "const"; name: string; init: TermForBasic; rest: TermForBasic }; 97 | 98 | export type TypeForBasic2 = 99 | | { tag: "Boolean" } 100 | | { tag: "Number" } 101 | | { tag: "Func"; params: ParamForBasic2[]; retType: TypeForBasic2 }; 102 | 103 | export type ParamForBasic2 = { name: string; type: TypeForBasic2 }; 104 | 105 | export type TermForBasic2 = 106 | | { tag: "true" } 107 | | { tag: "false" } 108 | | { tag: "if"; cond: TermForBasic2; thn: TermForBasic2; els: TermForBasic2 } 109 | | { tag: "number"; n: number } 110 | | { tag: "add"; left: TermForBasic2; right: TermForBasic2 } 111 | | { tag: "var"; name: string } 112 | | { tag: "func"; params: ParamForBasic2[]; body: TermForBasic2 } 113 | | { tag: "call"; func: TermForBasic2; args: TermForBasic2[] } 114 | | { tag: "seq2"; body: TermForBasic2[] } 115 | | { tag: "const2"; name: string; init: TermForBasic2 }; 116 | 117 | export type TypeForObj = 118 | | { tag: "Boolean" } 119 | | { tag: "Number" } 120 | | { tag: "Func"; params: ParamForObj[]; retType: TypeForObj } 121 | | { tag: "Object"; props: PropertyTypeForObj[] }; 122 | 123 | export type ParamForObj = { name: string; type: TypeForObj }; 124 | export type PropertyTypeForObj = { name: string; type: TypeForObj }; 125 | 126 | export type TermForObj = 127 | | { tag: "true" } 128 | | { tag: "false" } 129 | | { tag: "if"; cond: TermForObj; thn: TermForObj; els: TermForObj } 130 | | { tag: "number"; n: number } 131 | | { tag: "add"; left: TermForObj; right: TermForObj } 132 | | { tag: "var"; name: string } 133 | | { tag: "func"; params: ParamForObj[]; body: TermForObj } 134 | | { tag: "call"; func: TermForObj; args: TermForObj[] } 135 | | { tag: "seq"; body: TermForObj; rest: TermForObj } 136 | | { tag: "const"; name: string; init: TermForObj; rest: TermForObj } 137 | | { tag: "objectNew"; props: PropertyTermForObj[] } 138 | | { tag: "objectGet"; obj: TermForObj; propName: string }; 139 | 140 | export type PropertyTermForObj = { name: string; term: TermForObj }; 141 | 142 | export type TypeForTaggedUnion = 143 | | { tag: "Boolean" } 144 | | { tag: "Number" } 145 | | { tag: "Func"; params: ParamForTaggedUnion[]; retType: TypeForTaggedUnion } 146 | | { tag: "Object"; props: PropertyTypeForTaggedUnion[] } 147 | | { tag: "TaggedUnion"; variants: VariantTypeForTaggedUnion[] }; 148 | 149 | export type ParamForTaggedUnion = { name: string; type: TypeForTaggedUnion }; 150 | export type PropertyTypeForTaggedUnion = { name: string; type: TypeForTaggedUnion }; 151 | export type VariantTypeForTaggedUnion = { tagLabel: string; props: PropertyTypeForTaggedUnion[] }; 152 | 153 | export type TermForTaggedUnion = 154 | | { tag: "true" } 155 | | { tag: "false" } 156 | | { tag: "if"; cond: TermForTaggedUnion; thn: TermForTaggedUnion; els: TermForTaggedUnion } 157 | | { tag: "number"; n: number } 158 | | { tag: "add"; left: TermForTaggedUnion; right: TermForTaggedUnion } 159 | | { tag: "var"; name: string } 160 | | { tag: "func"; params: ParamForTaggedUnion[]; body: TermForTaggedUnion } 161 | | { tag: "call"; func: TermForTaggedUnion; args: TermForTaggedUnion[] } 162 | | { tag: "seq"; body: TermForTaggedUnion; rest: TermForTaggedUnion } 163 | | { tag: "const"; name: string; init: TermForTaggedUnion; rest: TermForTaggedUnion } 164 | | { tag: "objectNew"; props: PropertyTermForTaggedUnion[] } 165 | | { tag: "objectGet"; obj: TermForTaggedUnion; propName: string } 166 | | { tag: "taggedUnionNew"; tagLabel: string; props: PropertyTermForTaggedUnion[]; as: TypeForTaggedUnion } 167 | | { tag: "taggedUnionGet"; varName: string; clauses: VariantTermForTaggedUnion[] }; 168 | 169 | export type PropertyTermForTaggedUnion = { name: string; term: TermForTaggedUnion }; 170 | export type VariantTermForTaggedUnion = { tagLabel: string; term: TermForTaggedUnion }; 171 | 172 | export type TypeForRecFunc = 173 | | { tag: "Boolean" } 174 | | { tag: "Number" } 175 | | { tag: "Func"; params: ParamForRecFunc[]; retType: TypeForRecFunc }; 176 | 177 | export type ParamForRecFunc = { name: string; type: TypeForRecFunc }; 178 | 179 | export type TermForRecFunc = 180 | | { tag: "true" } 181 | | { tag: "false" } 182 | | { tag: "if"; cond: TermForRecFunc; thn: TermForRecFunc; els: TermForRecFunc } 183 | | { tag: "number"; n: number } 184 | | { tag: "add"; left: TermForRecFunc; right: TermForRecFunc } 185 | | { tag: "var"; name: string } 186 | | { tag: "func"; params: ParamForRecFunc[]; body: TermForRecFunc } 187 | | { tag: "call"; func: TermForRecFunc; args: TermForRecFunc[] } 188 | | { tag: "seq"; body: TermForRecFunc; rest: TermForRecFunc } 189 | | { tag: "const"; name: string; init: TermForRecFunc; rest: TermForRecFunc } 190 | | { 191 | tag: "recFunc"; 192 | funcName: string; 193 | params: ParamForRecFunc[]; 194 | retType: TypeForRecFunc; 195 | body: TermForRecFunc; 196 | rest: TermForRecFunc; 197 | }; 198 | 199 | export type TypeForSub = 200 | | { tag: "Boolean" } 201 | | { tag: "Number" } 202 | | { tag: "Func"; params: ParamForSub[]; retType: TypeForSub } 203 | | { tag: "Object"; props: PropertyTypeForSub[] }; 204 | 205 | export type ParamForSub = { name: string; type: TypeForSub }; 206 | export type PropertyTypeForSub = { name: string; type: TypeForSub }; 207 | 208 | export type TermForSub = 209 | | { tag: "true" } 210 | | { tag: "false" } 211 | //| { tag: "if"; cond: TermForSub; thn: TermForSub; els: TermForSub } 212 | | { tag: "number"; n: number } 213 | | { tag: "add"; left: TermForSub; right: TermForSub } 214 | | { tag: "var"; name: string } 215 | | { tag: "func"; params: ParamForSub[]; body: TermForSub } 216 | | { tag: "call"; func: TermForSub; args: TermForSub[] } 217 | | { tag: "seq"; body: TermForSub; rest: TermForSub } 218 | | { tag: "const"; name: string; init: TermForSub; rest: TermForSub } 219 | | { tag: "objectNew"; props: PropertyTermForSub[] } 220 | | { tag: "objectGet"; obj: TermForSub; propName: string }; 221 | 222 | export type PropertyTermForSub = { name: string; term: TermForSub }; 223 | 224 | export type TypeForRec = 225 | | { tag: "Boolean" } 226 | | { tag: "Number" } 227 | | { tag: "Func"; params: ParamForRec[]; retType: TypeForRec } 228 | | { tag: "Object"; props: PropertyTypeForRec[] } 229 | | { tag: "Rec"; name: string; type: TypeForRec } 230 | | { tag: "TypeVar"; name: string }; 231 | 232 | export type ParamForRec = { name: string; type: TypeForRec }; 233 | export type PropertyTypeForRec = { name: string; type: TypeForRec }; 234 | 235 | export type TermForRec = 236 | | { tag: "true" } 237 | | { tag: "false" } 238 | | { tag: "if"; cond: TermForRec; thn: TermForRec; els: TermForRec } 239 | | { tag: "number"; n: number } 240 | | { tag: "add"; left: TermForRec; right: TermForRec } 241 | | { tag: "var"; name: string } 242 | | { tag: "func"; params: ParamForRec[]; body: TermForRec } 243 | | { tag: "call"; func: TermForRec; args: TermForRec[] } 244 | | { tag: "seq"; body: TermForRec; rest: TermForRec } 245 | | { tag: "const"; name: string; init: TermForRec; rest: TermForRec } 246 | | { tag: "objectNew"; props: PropertyTermForRec[] } 247 | | { tag: "objectGet"; obj: TermForRec; propName: string } 248 | | { 249 | tag: "recFunc"; 250 | funcName: string; 251 | params: ParamForRec[]; 252 | retType: TypeForRec; 253 | body: TermForRec; 254 | rest: TermForRec; 255 | }; 256 | 257 | export type PropertyTermForRec = { name: string; term: TermForRec }; 258 | 259 | export type TypeForRec2 = 260 | | { tag: "Boolean" } 261 | | { tag: "Number" } 262 | | { tag: "Func"; params: ParamForRec2[]; retType: TypeForRec2 } 263 | | { tag: "Object"; props: PropertyTypeForRec2[] } 264 | | { tag: "TaggedUnion"; variants: VariantTypeForRec2[] } 265 | | { tag: "Rec"; name: string; type: TypeForRec2 } 266 | | { tag: "TypeVar"; name: string }; 267 | 268 | export type ParamForRec2 = { name: string; type: TypeForRec2 }; 269 | export type PropertyTypeForRec2 = { name: string; type: TypeForRec2 }; 270 | export type VariantTypeForRec2 = { tagLabel: string; props: PropertyTypeForRec2[] }; 271 | 272 | export type TermForRec2 = 273 | | { tag: "true" } 274 | | { tag: "false" } 275 | | { tag: "if"; cond: TermForRec2; thn: TermForRec2; els: TermForRec2 } 276 | | { tag: "number"; n: number } 277 | | { tag: "add"; left: TermForRec2; right: TermForRec2 } 278 | | { tag: "var"; name: string } 279 | | { tag: "func"; params: ParamForRec2[]; body: TermForRec2 } 280 | | { tag: "call"; func: TermForRec2; args: TermForRec2[] } 281 | | { tag: "seq"; body: TermForRec2; rest: TermForRec2 } 282 | | { tag: "const"; name: string; init: TermForRec2; rest: TermForRec2 } 283 | | { tag: "objectNew"; props: PropertyTermForRec2[] } 284 | | { tag: "objectGet"; obj: TermForRec2; propName: string } 285 | | { tag: "taggedUnionNew"; tagLabel: string; props: PropertyTermForRec2[]; as: TypeForRec2 } 286 | | { tag: "taggedUnionGet"; varName: string; clauses: VariantTermForRec2[] } 287 | | { 288 | tag: "recFunc"; 289 | funcName: string; 290 | params: ParamForRec2[]; 291 | retType: TypeForRec2; 292 | body: TermForRec2; 293 | rest: TermForRec2; 294 | }; 295 | 296 | export type PropertyTermForRec2 = { name: string; term: TermForRec2 }; 297 | export type VariantTermForRec2 = { tagLabel: string; term: TermForRec2 }; 298 | 299 | export type TypeForPoly = 300 | | { tag: "Boolean" } 301 | | { tag: "Number" } 302 | | { tag: "Func"; params: ParamForPoly[]; retType: TypeForPoly } 303 | | { tag: "TypeAbs"; typeParams: string[]; type: TypeForPoly } 304 | | { tag: "TypeVar"; name: string }; 305 | 306 | export type ParamForPoly = { name: string; type: TypeForPoly }; 307 | 308 | export type TermForPoly = 309 | | { tag: "true" } 310 | | { tag: "false" } 311 | | { tag: "if"; cond: TermForPoly; thn: TermForPoly; els: TermForPoly } 312 | | { tag: "number"; n: number } 313 | | { tag: "add"; left: TermForPoly; right: TermForPoly } 314 | | { tag: "var"; name: string } 315 | | { tag: "func"; params: ParamForPoly[]; body: TermForPoly } 316 | | { tag: "call"; func: TermForPoly; args: TermForPoly[] } 317 | | { tag: "seq"; body: TermForPoly; rest: TermForPoly } 318 | | { tag: "const"; name: string; init: TermForPoly; rest: TermForPoly } 319 | | { tag: "typeAbs"; typeParams: string[]; body: TermForPoly } 320 | | { tag: "typeApp"; typeAbs: TermForPoly; typeArgs: TypeForPoly[] }; 321 | // End of automatically generated code 322 | 323 | type TypeForSelf = 324 | | { tag: "Boolean" } 325 | | { tag: "Number" } 326 | | { tag: "String" } 327 | | { tag: "Option"; elemType: TypeForSelf } 328 | | { tag: "Array"; elemType: TypeForSelf } 329 | | { tag: "Record"; elemType: TypeForSelf } 330 | | { tag: "Func"; params: ParamForSelf[]; retType: TypeForSelf } 331 | | { tag: "Object"; props: PropertyTypeForSelf[] } 332 | | { tag: "TaggedUnion"; variants: VariantTypeForSelf[] } 333 | | { tag: "Rec"; name: string; type: TypeForSelf } 334 | | { tag: "TypeVar"; name: string } 335 | | { tag: "Undefined" }; 336 | 337 | type ParamForSelf = { name: string; type: TypeForSelf }; 338 | type PropertyTypeForSelf = { name: string; type: TypeForSelf }; 339 | type VariantTypeForSelf = { tagLabel: string; props: PropertyTypeForSelf[] }; 340 | 341 | type TermForSelf = 342 | | { tag: "true" } 343 | | { tag: "false" } 344 | | { tag: "not"; cond: TermForSelf } 345 | | { tag: "compare"; op: string; left: TermForSelf; right: TermForSelf } 346 | | { tag: "if"; cond: TermForSelf; thn: TermForSelf; els: TermForSelf } 347 | | { tag: "number"; n: number } 348 | | { tag: "add"; left: TermForSelf; right: TermForSelf } 349 | | { tag: "string"; s: string } 350 | | { tag: "var"; name: string } 351 | | { tag: "func"; params: ParamForSelf[]; body: TermForSelf } 352 | | { tag: "call"; func: TermForSelf; args: TermForSelf[] } 353 | | { tag: "seq"; body: TermForSelf; rest: TermForSelf } 354 | | { tag: "const"; name: string; init: TermForSelf; rest: TermForSelf } 355 | | { tag: "assign"; name: string; init: TermForSelf } 356 | | { tag: "for"; ary: TermForSelf; body: TermForSelf; idx: string; rest: TermForSelf } 357 | | { tag: "forOf"; ary: TermForSelf; body: TermForSelf; var: string; rest: TermForSelf } 358 | | { tag: "arrayNew"; aryType: TypeForSelf } 359 | | { tag: "arrayExt"; ary: TermForSelf; val: TermForSelf } 360 | | { tag: "recordNew"; recordType: TypeForSelf } 361 | | { tag: "recordCopy"; record: TermForSelf } 362 | | { tag: "recordExt"; record: TermForSelf; key: TermForSelf; val: TermForSelf } 363 | | { tag: "recordIn"; record: TermForSelf; key: TermForSelf } 364 | | { tag: "member"; base: TermForSelf; index: TermForSelf } 365 | | { tag: "objectNew"; props: PropertyTermForSelf[] } 366 | | { tag: "objectGet"; obj: TermForSelf; propName: string } 367 | | { tag: "taggedUnionNew"; tagLabel: string; props: PropertyTermForSelf[]; as: TypeForSelf } 368 | | { tag: "taggedUnionExt"; term: TermForSelf; as: TypeForSelf } 369 | | { tag: "taggedUnionGet"; varName: string; clauses: VariantTermForSelf[]; defaultClause: TermForSelf } 370 | | { 371 | tag: "recFunc"; 372 | funcName: string; 373 | params: ParamForSelf[]; 374 | retType: TypeForSelf; 375 | body: TermForSelf; 376 | rest: TermForSelf; 377 | } 378 | | { tag: "undefined" }; 379 | 380 | type PropertyTermForSelf = { name: string; term: TermForSelf }; 381 | type VariantTermForSelf = { tagLabel: string; term: TermForSelf }; 382 | 383 | // --- 384 | // Preprocessing of type variables. 385 | // Replace type aliases with their definitions. 386 | // If recursive, convert it to a recursive type. 387 | 388 | type TypeAliasMap = Record; 389 | type TypeVarBindings = Record; 390 | type Context = { 391 | globalTypeAliasMap: TypeAliasMap; 392 | typeVarBindings: TypeVarBindings; 393 | }; 394 | 395 | // Get all free type variables in a type 396 | function freeTypeVars(ty: Type): Set { 397 | switch (ty.tag) { 398 | case "Boolean": 399 | case "Number": 400 | case "String": 401 | return new Set(); 402 | case "Option": 403 | return freeTypeVars(ty.elemType); 404 | case "Array": 405 | return freeTypeVars(ty.elemType); 406 | case "Record": 407 | return freeTypeVars(ty.elemType); 408 | case "Func": 409 | return ty.params.reduce((r, { type }) => r.union(freeTypeVars(type)), freeTypeVars(ty.retType)); 410 | case "Object": 411 | return ty.props.reduce((r, { type }) => r.union(freeTypeVars(type)), new Set()); 412 | case "TaggedUnion": 413 | return ty.variants.reduce((r, { props }) => { 414 | return props.reduce((r, { type }) => r.union(freeTypeVars(type)), r); 415 | }, new Set()); 416 | case "Rec": 417 | return freeTypeVars(ty.type).difference(new Set([ty.name])); 418 | case "TypeAbs": 419 | return freeTypeVars(ty.type).difference(new Set(ty.typeParams)); 420 | case "TypeVar": 421 | return new Set([ty.name]); 422 | case "Undefined": 423 | return new Set(); 424 | } 425 | } 426 | 427 | // Add type variables to the context 428 | function extendContextWithTypeVars(ctx: Context, tyVars: string[]): Context { 429 | return { 430 | globalTypeAliasMap: ctx.globalTypeAliasMap, 431 | typeVarBindings: tyVars.reduce( 432 | (bindings, name) => ({ ...bindings, [name]: { tag: "TypeVar", name } }), 433 | ctx.typeVarBindings, 434 | ), 435 | }; 436 | } 437 | 438 | // Expand type variables in a type 439 | function expandTypeAliases(ty: Type, recDefined: Set, ctx: Context): Type { 440 | switch (ty.tag) { 441 | case "Boolean": 442 | return { tag: "Boolean" }; 443 | case "Number": 444 | return { tag: "Number" }; 445 | case "String": 446 | return { tag: "String" }; 447 | case "Option": 448 | return { tag: "Option", elemType: expandTypeAliases(ty.elemType, recDefined, ctx) }; 449 | case "Array": 450 | return { tag: "Array", elemType: expandTypeAliases(ty.elemType, recDefined, ctx) }; 451 | case "Record": 452 | return { tag: "Record", elemType: expandTypeAliases(ty.elemType, recDefined, ctx) }; 453 | case "Func": { 454 | const params = ty.params.map(({ name, type }) => ({ name, type: expandTypeAliases(type, recDefined, ctx) })); 455 | const retType = expandTypeAliases(ty.retType, recDefined, ctx); 456 | return { tag: "Func", params, retType }; 457 | } 458 | case "Object": { 459 | const props = ty.props.map(({ name, type }) => ({ name, type: expandTypeAliases(type, recDefined, ctx) })); 460 | return { tag: "Object", props }; 461 | } 462 | case "TaggedUnion": { 463 | const variants = ty.variants.map(({ tagLabel, props }) => ({ 464 | tagLabel, 465 | props: props.map(({ name, type }) => ({ name, type: expandTypeAliases(type, recDefined, ctx) })), 466 | })); 467 | return { tag: "TaggedUnion", variants }; 468 | } 469 | case "Rec": { 470 | const newCtx = extendContextWithTypeVars(ctx, [ty.name]); 471 | return { tag: "Rec", name: ty.name, type: expandTypeAliases(ty.type, recDefined, newCtx) }; 472 | } 473 | case "TypeAbs": { 474 | const newCtx = extendContextWithTypeVars(ctx, ty.typeParams); 475 | return { tag: "TypeAbs", typeParams: ty.typeParams, type: expandTypeAliases(ty.type, recDefined, newCtx) }; 476 | } 477 | case "TypeVar": { 478 | const typeArgs = ty.typeArgs; 479 | if (typeArgs) { 480 | if (ty.name in ctx.typeVarBindings) error(`not a generic type: ${ty.name}`, ty); 481 | if (recDefined.has(ty.name)) error(`type recursion for generics is not supported`, ty); 482 | if (!(ty.name in ctx.globalTypeAliasMap)) error(`unbound type variable: ${ty.name}`, ty); 483 | const { typeParams, type } = ctx.globalTypeAliasMap[ty.name]; 484 | if (!typeParams) error(`not a generic type: ${ty.name}`, ty); 485 | if (typeParams.length !== typeArgs.length) error(`wrong number of type arguments for ${ty.name}`, ty); 486 | const typeVarBindings = typeParams.reduce((bindings, typeParam, i) => { 487 | return { ...bindings, [typeParam]: expandTypeAliases(typeArgs[i], recDefined, ctx) }; 488 | }, ctx.typeVarBindings); 489 | const newCtx: Context = { 490 | globalTypeAliasMap: ctx.globalTypeAliasMap, 491 | typeVarBindings, 492 | }; 493 | const retTy = expandTypeAliases(type, recDefined.union(new Set([ty.name])), newCtx); 494 | if (freeTypeVars(retTy).has(ty.name)) error("bug?", ty); 495 | return retTy; 496 | } else { 497 | if (ty.name in ctx.typeVarBindings) return ctx.typeVarBindings[ty.name]; 498 | if (recDefined.has(ty.name)) return { tag: "TypeVar", name: ty.name }; 499 | if (!(ty.name in ctx.globalTypeAliasMap)) error(`unbound type variable: ${ty.name}`, ty); 500 | const { typeParams, type } = ctx.globalTypeAliasMap[ty.name]; 501 | if (typeParams) error(`type arguments are required for ${ty.name}`, ty); 502 | const newCtx: Context = { 503 | globalTypeAliasMap: ctx.globalTypeAliasMap, 504 | typeVarBindings: {}, 505 | }; 506 | const retTy = expandTypeAliases(type, recDefined.union(new Set([ty.name])), newCtx); 507 | return freeTypeVars(retTy).has(ty.name) ? { tag: "Rec", name: ty.name, type: retTy } : retTy; 508 | } 509 | } 510 | case "Undefined": 511 | return { tag: "Undefined" }; 512 | } 513 | } 514 | 515 | // Replace type variables in a type 516 | function simplifyType(ty: Type, ctx: Context) { 517 | return expandTypeAliases(ty, new Set(), ctx); 518 | } 519 | 520 | // --- 521 | // Convert estree nodes to our simplified node definition 522 | 523 | // Helper functions to get properties of estree nodes 524 | 525 | function getIdentifier(node: p.TSESTree.Expression | p.TSESTree.PrivateIdentifier | p.TSESTree.TSQualifiedName) { 526 | if (node.type !== "Identifier") error("identifier expected", node); 527 | return node.name; 528 | } 529 | 530 | function getLiteralString(node: p.TSESTree.Expression) { 531 | if (node.type !== "Literal" || typeof node.value !== "string") error("", node); 532 | return node.value; 533 | } 534 | 535 | function getTypeProp(member: p.TSESTree.TypeElement) { 536 | if (member.type !== "TSPropertySignature" || member.key.type !== "Identifier" || !member.typeAnnotation) { 537 | error("object type must have only normal key-value paris", member); 538 | } 539 | return { key: member.key.name, type: member.typeAnnotation.typeAnnotation }; 540 | } 541 | 542 | function getProp(property: p.TSESTree.ObjectLiteralElement) { 543 | if (property.type === "SpreadElement") error("spread operator is not allowed", property); 544 | if (property.computed) error("key must be bare", property); 545 | if (property.value.type === "AssignmentPattern") error("AssignmentPattern is not allowed", property); 546 | if (property.value.type === "TSEmptyBodyFunctionExpression") { 547 | error("TSEmptyBodyFunctionExpression is not allowed", property); 548 | } 549 | return { key: getIdentifier(property.key), value: property.value }; 550 | } 551 | 552 | function getTagAndProps(node: p.TSESTree.ObjectExpression) { 553 | const props = node.properties.map(getProp); 554 | const tag = props.find(({ key }) => key == "tag"); 555 | if (!tag) error(`tagged union must have a "tag" key`, node); 556 | const props2 = props.filter(({ key }) => key != "tag"); 557 | 558 | return { tag: getLiteralString(tag.value), props: props2 }; 559 | } 560 | 561 | function getSwitchVarName(node: p.TSESTree.Expression) { 562 | if (node.type !== "MemberExpression" || node.computed || getIdentifier(node.property) !== "tag") { 563 | error(`switch statement must be switch(VAR.tag) { ... }`, node); 564 | } 565 | return getIdentifier(node.object); 566 | } 567 | 568 | function getParam(node: p.TSESTree.Parameter): Param { 569 | if (node.type !== "Identifier") error("parameter must be a variable", node); 570 | if (!node.typeAnnotation) error("parameter type is required", node); 571 | const name = node.name; 572 | const type = convertType(node.typeAnnotation.typeAnnotation); 573 | return { name, type }; 574 | } 575 | 576 | function getRecFunc(node: p.TSESTree.FunctionDeclaration, ctx: Context, restFunc: (() => Term) | null): Term { 577 | if (!node.id) error("function name is required", node); 578 | if (!node.returnType) error("function return type is required", node); 579 | const funcName = node.id.name; 580 | if (node.typeParameters) error("type parameter is not supported for function declaration", node); 581 | const params = node.params.map((param) => { 582 | const { name, type } = getParam(param); 583 | return { name, type: simplifyType(type, ctx) }; 584 | }); 585 | const retType = simplifyType(convertType(node.returnType.typeAnnotation), ctx); 586 | const body = convertStmts(node.body.body, true, ctx); 587 | const rest: Term = restFunc ? restFunc() : { tag: "var", name: funcName, loc: node.loc }; 588 | return { tag: "recFunc", funcName, params, retType, body, rest, loc: node.loc }; 589 | } 590 | 591 | // Convert estree type node to our simplified node definition 592 | function convertType(node: p.TSESTree.TypeNode): Type { 593 | switch (node.type) { 594 | case "TSBooleanKeyword": 595 | return { tag: "Boolean", loc: node.loc }; 596 | case "TSNumberKeyword": 597 | return { tag: "Number", loc: node.loc }; 598 | case "TSStringKeyword": 599 | return { tag: "String", loc: node.loc }; 600 | case "TSArrayType": { 601 | return { tag: "Array", elemType: convertType(node.elementType), loc: node.loc }; 602 | } 603 | case "TSFunctionType": { 604 | if (!node.returnType) error("return type is required", node); 605 | const params = node.params.map((param) => getParam(param)); 606 | const retType = convertType(node.returnType.typeAnnotation); 607 | const funcTy: Type = { tag: "Func", params, retType }; 608 | if (!node.typeParameters) return funcTy; 609 | const typeParams = node.typeParameters.params.map((typeParameter) => typeParameter.name.name); 610 | if (new Set(typeParams).size !== typeParams.length) error("duplicate type parameters", node); 611 | return { tag: "TypeAbs", typeParams, type: funcTy, loc: node.loc }; 612 | } 613 | case "TSTypeLiteral": { 614 | const props = node.members.map((member) => { 615 | const { key, type } = getTypeProp(member); 616 | return { name: key, type: convertType(type) }; 617 | }); 618 | return { tag: "Object", props, loc: node.loc }; 619 | } 620 | case "TSUnionType": { 621 | const variants = node.types.map((variant) => { 622 | if (variant.type !== "TSTypeLiteral") { 623 | error(`tagged union type must be { tag: "TAG", ... }`, variant); 624 | } 625 | const variant2 = variant.members.map(getTypeProp); 626 | const found = variant2.find(({ key }) => key === "tag"); 627 | if (!found) { 628 | error(`tagged union type must be { tag: "TAG", ... }`, variant); 629 | } 630 | const { type: tagType } = found; 631 | if (tagType.type !== "TSLiteralType") { 632 | error(`tagged union type must be { tag: "TAG", ... }`, variant); 633 | } 634 | const tagLabel = getLiteralString(tagType.literal); 635 | const variant3 = variant2.filter(({ key }) => key !== "tag"); 636 | const props = variant3.map(({ key, type }) => ({ name: key, type: convertType(type) })); 637 | return { tagLabel, props }; 638 | }); 639 | return { tag: "TaggedUnion", variants, loc: node.loc }; 640 | } 641 | case "TSTypeReference": { 642 | const typeArgs = node.typeArguments?.params.map((tyArg) => convertType(tyArg)); 643 | const name = getIdentifier(node.typeName); 644 | if (name === "Record") { 645 | if (!typeArgs || typeArgs.length !== 2) error("Record must have 2 type arguments", node); 646 | if (typeArgs[0].tag !== "String") error("Record must be Record", node); 647 | return { tag: "Record", elemType: typeArgs[1], loc: node.loc }; 648 | } 649 | return { tag: "TypeVar", name, typeArgs, loc: node.loc }; 650 | } 651 | default: 652 | error(`unknown node: ${node.type}`, node); 653 | } 654 | } 655 | 656 | function newIfNode(cond: Term, thn: Term, els: Term, loc: Location): Term { 657 | if (cond.tag === "compare") { 658 | if (cond.left.tag === "objectGet" && cond.left.obj.tag === "var" && cond.left.propName == "tag") { 659 | if (cond.right.tag === "string") { 660 | const varName = cond.left.obj.name; 661 | const tagLabel = cond.right.s; 662 | if (cond.op === "===") { 663 | return { tag: "taggedUnionGet", varName, clauses: [{ tagLabel, term: thn }], defaultClause: els, loc }; 664 | } else if (cond.op === "!==") { 665 | return { tag: "taggedUnionGet", varName, clauses: [{ tagLabel, term: els }], defaultClause: thn, loc }; 666 | } 667 | } 668 | } 669 | } 670 | return { tag: "if", cond, thn, els, loc }; 671 | } 672 | 673 | // Convert estree expression node to our simplified node definition 674 | function convertExpr(node: p.TSESTree.Expression, ctx: Context): Term { 675 | switch (node.type) { 676 | case "UnaryExpression": { 677 | if (node.operator !== "!") error(`unsupported operator: ${node.operator}`, node); 678 | const body = convertExpr(node.argument, ctx); 679 | return { tag: "not", cond: body, loc: node.loc }; 680 | } 681 | // deno-lint-ignore no-fallthrough 682 | case "BinaryExpression": { 683 | if (node.left.type === "PrivateIdentifier") error("private identifer is not allowed", node.left); 684 | switch (node.operator) { 685 | case "+": { 686 | const left = convertExpr(node.left, ctx); 687 | const right = convertExpr(node.right, ctx); 688 | return { tag: "add", left, right, loc: node.loc }; 689 | } 690 | case "===": 691 | case "!==": { 692 | const left = convertExpr(node.left, ctx); 693 | const right = convertExpr(node.right, ctx); 694 | return { tag: "compare", op: node.operator, left, right, loc: node.loc }; 695 | } 696 | case "in": { 697 | const key = convertExpr(node.left, ctx); 698 | const record = convertExpr(node.right, ctx); 699 | return { tag: "recordIn", record, key, loc: node.loc }; 700 | } 701 | default: 702 | error(`unsupported operator: ${node.operator}`, node); 703 | } 704 | } 705 | case "Identifier": 706 | return { tag: "var", name: node.name, loc: node.loc }; 707 | // deno-lint-ignore no-fallthrough 708 | case "Literal": 709 | switch (typeof node.value) { 710 | case "number": 711 | return { tag: "number", n: node.value, loc: node.loc }; 712 | case "boolean": 713 | return { tag: node.value ? "true" : "false", loc: node.loc }; 714 | case "string": 715 | return { tag: "string", s: node.value, loc: node.loc }; 716 | default: 717 | error(`unsupported literal: ${node.value}`, node); 718 | } 719 | case "ArrowFunctionExpression": { 720 | const typeParams = node.typeParameters?.params.map((typeParameter) => typeParameter.name.name); 721 | const newCtx = typeParams ? extendContextWithTypeVars(ctx, typeParams) : ctx; 722 | const params = node.params.map((param) => { 723 | const { name, type } = getParam(param); 724 | return { name, type: simplifyType(type, newCtx) }; 725 | }); 726 | let retType; 727 | if (node.returnType) { 728 | retType = simplifyType(convertType(node.returnType.typeAnnotation), ctx); 729 | } 730 | const body = node.body.type === "BlockStatement" 731 | ? convertStmts(node.body.body, true, newCtx) 732 | : convertExpr(node.body, newCtx); 733 | const func: Term = { tag: "func", params, ...(retType !== undefined ? { retType } : {}), body, loc: node.loc }; 734 | if (typeParams && new Set(typeParams).size !== typeParams.length) error("duplicate type parameters", node); 735 | return typeParams ? { tag: "typeAbs", typeParams, body: func, loc: node.typeParameters!.loc } : func; 736 | } 737 | case "CallExpression": { 738 | const args = node.arguments.map((argument) => { 739 | if (argument.type === "SpreadElement") error("argument must be an expression", argument); 740 | return convertExpr(argument, ctx); 741 | }); 742 | let func = convertExpr(node.callee, ctx); 743 | if (node.typeArguments) { 744 | const typeArgs = node.typeArguments.params.map((param) => simplifyType(convertType(param), ctx)); 745 | func = { tag: "typeApp", typeAbs: func, typeArgs, loc: node.loc }; 746 | } 747 | return { tag: "call", func, args, loc: node.loc }; 748 | } 749 | case "TSAsExpression": 750 | case "TSSatisfiesExpression": 751 | case "TSTypeAssertion": { 752 | switch (node.expression.type) { 753 | case "ObjectExpression": { 754 | const ty = simplifyType(convertType(node.typeAnnotation), ctx); 755 | if (node.expression.properties.length === 0) { 756 | return { tag: "recordNew", recordType: ty, loc: node.loc }; 757 | } 758 | const { tag, props } = getTagAndProps(node.expression); 759 | const props2: PropertyTerm[] = []; 760 | props.forEach(({ key, value }) => { 761 | props2.push({ name: key, term: convertExpr(value, ctx) }); 762 | }); 763 | return { tag: "taggedUnionNew", tagLabel: tag, props: props2, as: ty, loc: node.loc }; 764 | } 765 | case "ArrayExpression": { 766 | const ty = simplifyType(convertType(node.typeAnnotation), ctx); 767 | if (node.expression.elements.length !== 0) { 768 | error("type-asserted array literal must be empty", node.expression.elements); 769 | } 770 | return { tag: "arrayNew", aryType: ty, loc: node.loc }; 771 | } 772 | default: { 773 | const ty = simplifyType(convertType(node.typeAnnotation), ctx); 774 | const term = convertExpr(node.expression, ctx); 775 | return { tag: "taggedUnionExt", term, as: ty, loc: node.loc }; 776 | } 777 | } 778 | } 779 | case "MemberExpression": { 780 | if (node.computed) { 781 | const base = convertExpr(node.object, ctx); 782 | const index = convertExpr(node.property, ctx); 783 | return { tag: "member", base, index, loc: node.loc }; 784 | } 785 | if (node.property.type !== "Identifier") error("object member must be OBJ.STR", node.property); 786 | const obj = convertExpr(node.object, ctx); 787 | const propName = node.property.name; 788 | return { tag: "objectGet", obj, propName, loc: node.loc }; 789 | } 790 | case "ArrayExpression": { 791 | if (node.elements.length !== 2 || !node.elements[0] || node.elements[0].type !== "SpreadElement") { 792 | error(`array must be "[...ary, val]"`, node); 793 | } 794 | if (!node.elements[1] || node.elements[1].type === "SpreadElement") { 795 | error(`array must be "[...ary, val]"`, node); 796 | } 797 | const base = convertExpr(node.elements[0].argument, ctx); 798 | const val = convertExpr(node.elements[1], ctx); 799 | return { tag: "arrayExt", ary: base, val, loc: node.loc }; 800 | } 801 | case "ObjectExpression": { 802 | if (node.properties.length === 0) error("bare empty object is not allowed", node); 803 | if (node.properties.length <= 2) { 804 | if (node.properties[0].type === "SpreadElement") { 805 | const record = convertExpr(node.properties[0].argument, ctx); 806 | if (node.properties.length === 1) { 807 | return { tag: "recordCopy", record, loc: node.loc }; 808 | } 809 | if (node.properties[1].type !== "SpreadElement") { 810 | const property = node.properties[1]; 811 | if (!property.computed) error(`record extension must be "{ ...record, [key]: value }"`, property); 812 | const key = convertExpr(property.key, ctx); 813 | if (property.value.type === "AssignmentPattern") error("AssignmentPattern is not allowed", property); 814 | if (property.value.type === "TSEmptyBodyFunctionExpression") { 815 | error("TSEmptyBodyFunctionExpression is not allowed", property); 816 | } 817 | const val = convertExpr(property.value, ctx); 818 | return { tag: "recordExt", record, key, val, loc: node.loc }; 819 | } 820 | } 821 | } 822 | const props = node.properties.map((property) => { 823 | const { key: name, value } = getProp(property); 824 | return { name, term: convertExpr(value, ctx) }; 825 | }); 826 | return { tag: "objectNew", props, loc: node.loc }; 827 | } 828 | case "AssignmentExpression": { 829 | if (node.operator !== "=") error(`unsupported operator: ${node.operator}`, node); 830 | const varName = getIdentifier(node.left); 831 | const init = convertExpr(node.right, ctx); 832 | return { tag: "assign", name: varName, init, loc: node.loc }; 833 | } 834 | case "ConditionalExpression": { 835 | const cond = convertExpr(node.test, ctx); 836 | const thn = convertExpr(node.consequent, ctx); 837 | if (!node.alternate) error("else clause is requried", node); 838 | const els = convertExpr(node.alternate, ctx); 839 | return newIfNode(cond, thn, els, node.loc); 840 | } 841 | case "TSNonNullExpression": 842 | return convertExpr(node.expression, ctx); 843 | case "TSInstantiationExpression": { 844 | const typeArgs = node.typeArguments.params.map((param) => simplifyType(convertType(param), ctx)); 845 | const typeAbs = convertExpr(node.expression, ctx); 846 | return { tag: "typeApp", typeAbs, typeArgs, loc: node.loc }; 847 | } 848 | default: 849 | error(`unsupported expression node: ${node.type}`, node); 850 | } 851 | } 852 | 853 | // Convert estree statement nodes to our simplified node definition 854 | function convertStmts(nodes: p.TSESTree.Statement[], requireReturn: boolean, ctx: Context): Term { 855 | function convertStmt(i: number, ctx: Context): Term { 856 | const last = nodes.length - 1 === i; 857 | const node = nodes[i]; 858 | switch (node.type) { 859 | case "VariableDeclaration": { 860 | if (last && requireReturn) error("return is required", node); 861 | if (node.declarations.length !== 1) error("multiple variable declaration is not allowed", node); 862 | const decl = node.declarations[0]; 863 | if (!decl.init) error("variable initializer is required", decl); 864 | const name = getIdentifier(decl.id); 865 | const init = convertExpr(decl.init, ctx); 866 | const rest: Term = last ? { tag: "var", name, loc: node.loc } : convertStmt(i + 1, ctx); 867 | return { tag: "const", name, init, rest, loc: node.loc }; 868 | } 869 | case "ExportNamedDeclaration": { 870 | if (last && requireReturn) error("return is required", node); 871 | if (!node.declaration || node.declaration.type !== "FunctionDeclaration") { 872 | error("export must have a function declaration", node); 873 | } 874 | return getRecFunc(node.declaration, ctx, last ? null : () => convertStmt(i + 1, ctx)); 875 | } 876 | case "FunctionDeclaration": { 877 | if (last && requireReturn) error("return is required", node); 878 | return getRecFunc(node, ctx, last ? null : () => convertStmt(i + 1, ctx)); 879 | } 880 | case "ExpressionStatement": { 881 | if ( 882 | node.expression.type === "CallExpression" && node.expression.callee.type === "Identifier" && 883 | node.expression.callee.name === "error" 884 | ) { 885 | return { tag: "undefined", loc: node.loc }; 886 | } 887 | const body = convertExpr(node.expression, ctx); 888 | if (last && !requireReturn) return body; 889 | const rest: Term = last ? { tag: "undefined", loc: node.loc } : convertStmt(i + 1, ctx); 890 | return { tag: "seq", body, rest, loc: node.loc }; 891 | } 892 | case "ReturnStatement": { 893 | if (!node.argument) error("return must have an argument", node); 894 | return convertExpr(node.argument, ctx); 895 | } 896 | case "IfStatement": { 897 | const cond = convertExpr(node.test, ctx); 898 | const thn = node.consequent.type == "BlockStatement" 899 | ? convertStmts(node.consequent.body, requireReturn, ctx) 900 | : convertStmts([node.consequent], requireReturn, ctx); 901 | let els: Term; 902 | if (!node.alternate) { 903 | if (!requireReturn) error("else clause is requried", node); 904 | els = nodes[i + 1] ? convertStmt(i + 1, ctx) : { tag: "undefined", loc: node.loc }; 905 | } else { 906 | els = convertStmts( 907 | node.alternate.type == "BlockStatement" ? node.alternate.body : [node.alternate], 908 | requireReturn, 909 | ctx, 910 | ); 911 | } 912 | return newIfNode(cond, thn, els, node.loc); 913 | } 914 | case "ForOfStatement": { 915 | if (!requireReturn) error("for statement is not allowed", node); 916 | if (node.left.type !== "VariableDeclaration") error(`for statement must be "for (let i of ary)"`, node); 917 | const varName = getIdentifier(node.left.declarations[0].id); 918 | const ary = convertExpr(node.right, ctx); 919 | if (node.body.type !== "BlockStatement") { 920 | error(`for statement must be "for (let i of ary)"`, node); 921 | } 922 | const body = convertStmts(node.body.body, true, ctx); 923 | const rest: Term = nodes[i + 1] ? convertStmt(i + 1, ctx) : { tag: "undefined", loc: node.loc }; 924 | return { tag: "forOf", ary, var: varName, body, rest, loc: node.loc }; 925 | } 926 | case "ForStatement": { 927 | if (!requireReturn) error("for statement is not allowed", node); 928 | if (!node.init || node.init.type !== "VariableDeclaration") { 929 | error(`for statement must be "for (let i = 0; i < ary.length; i++)"`, node); 930 | } 931 | const inits = node.init; 932 | if (inits.declarations.length !== 1) { 933 | error(`for statement must be "for (let i = 0; i < ary.length; i++)"`, node); 934 | } 935 | const idxVarName = getIdentifier(inits.declarations[0].id); 936 | const init = inits.declarations[0].init; 937 | if (!init || init.type !== "Literal" || init.value !== 0) { 938 | error(`for statement must be "for (let i = 0; i < ary.length; i++)"`, node); 939 | } 940 | if (!node.test || node.test.type !== "BinaryExpression" || node.test.operator !== "<") { 941 | error(`for statement must be "for (let i = 0; i < ary.length; i++)"`, node); 942 | } 943 | const testLeft = node.test.left; 944 | if (testLeft.type !== "Identifier" || testLeft.name !== idxVarName) { 945 | error(`for statement must be "for (let i = 0; i < ary.length; i++)"`, node); 946 | } 947 | const testRight = node.test.right; 948 | if ( 949 | testRight.type !== "MemberExpression" || testRight.computed || testRight.property.type !== "Identifier" || 950 | testRight.property.name !== "length" 951 | ) { 952 | error(`for statement must be "for (let i = 0; i < ary.length; i++)"`, node); 953 | } 954 | const ary = convertExpr(testRight.object, ctx); 955 | if (!node.update || node.update.type !== "UpdateExpression" || node.update.operator !== "++") { 956 | error(`for statement must be "for (let i = 0; i < ary.length; i++)"`, node); 957 | } 958 | if (node.update.argument.type !== "Identifier" || node.update.argument.name !== idxVarName) { 959 | error(`for statement must be "for (let i = 0; i < ary.length; i++)"`, node); 960 | } 961 | if (node.body.type !== "BlockStatement") { 962 | error(`for statement must be "for (let i = 0; i < ary.length; i++)"`, node); 963 | } 964 | const body = convertStmts(node.body.body, true, ctx); 965 | const rest: Term = nodes[i + 1] ? convertStmt(i + 1, ctx) : { tag: "undefined", loc: node.loc }; 966 | return { tag: "for", ary, idx: idxVarName, body, rest, loc: node.loc }; 967 | } 968 | case "SwitchStatement": { 969 | const varName = getSwitchVarName(node.discriminant); 970 | const clauses: VariantTerm[] = []; 971 | let defaultClause: Term = { tag: "undefined", loc: node.loc }; 972 | node.cases.forEach((caseNode) => { 973 | const conseq = caseNode.consequent; 974 | const stmts = conseq.length === 1 && conseq[0].type === "BlockStatement" ? conseq[0].body : conseq; 975 | const clause = convertStmts(stmts, requireReturn, ctx); 976 | if (caseNode.test) { 977 | clauses.push({ tagLabel: getLiteralString(caseNode.test), term: clause }); 978 | } else { 979 | defaultClause = clause; 980 | } 981 | }); 982 | return { tag: "taggedUnionGet", varName, clauses, defaultClause, loc: node.loc }; 983 | } 984 | case "ThrowStatement": { 985 | return { tag: "undefined", loc: node.loc }; 986 | } 987 | case "EmptyStatement": { 988 | return convertStmt(i + 1, ctx); 989 | } 990 | default: 991 | error(`unsupported statement node: ${node.type}`, node); 992 | } 993 | } 994 | return convertStmt(0, ctx); 995 | } 996 | 997 | // Convert estree program nodes to our simplified node definition 998 | function convertProgram(nodes: p.TSESTree.Statement[]): Term { 999 | const globalTypeAliasMap: TypeAliasMap = {}; 1000 | 1001 | const stmts = nodes.filter((node) => { 1002 | switch (node.type) { 1003 | case "ImportDeclaration": 1004 | return false; 1005 | case "TSTypeAliasDeclaration": { 1006 | const name = node.id.name; 1007 | const typeParams = node.typeParameters ? node.typeParameters.params.map((param) => param.name.name) : null; 1008 | const type = convertType(node.typeAnnotation); 1009 | globalTypeAliasMap[name] = { typeParams, type }; 1010 | return false; 1011 | } 1012 | default: 1013 | return true; 1014 | } 1015 | }); 1016 | 1017 | const ctx = { globalTypeAliasMap, typeVarBindings: {} }; 1018 | 1019 | return convertStmts(stmts, false, ctx); 1020 | } 1021 | 1022 | // --- 1023 | // Parse functions 1024 | 1025 | type ExtractTagsSub = T extends Type | Term ? ExtractTags 1026 | : T extends Type[] ? ExtractTags[] 1027 | : T extends Term[] ? ExtractTags[] 1028 | : T extends (infer U)[] ? { [P in keyof U]: ExtractTags }[] 1029 | : T; 1030 | type ExtractTags = Extract<{ [P in keyof T]: ExtractTagsSub }, { tag: K }>; 1031 | 1032 | function subsetSystem( 1033 | node: Term, 1034 | keepTypes: Types[], 1035 | keepTerms: Terms[], 1036 | keepDefaultClause: boolean = false, 1037 | ): ExtractTags { 1038 | // deno-lint-ignore no-explicit-any 1039 | if (!node.tag) return node as any; 1040 | 1041 | if (!(keepTypes as string[]).includes(node.tag) && !(keepTerms as string[]).includes(node.tag)) { 1042 | throw new Error(`"${node.tag}" is not allowed in this system`); 1043 | } 1044 | 1045 | // deno-lint-ignore no-explicit-any 1046 | const newNode: any = {}; 1047 | Object.entries(node).forEach(([key, val]) => { 1048 | if (!keepDefaultClause && key === "defaultClause") return; 1049 | if (typeof val !== "object" || !val.tag) { 1050 | newNode[key] = val; 1051 | } else { 1052 | newNode[key] = subsetSystem(val, keepTypes, keepTerms, keepDefaultClause); 1053 | } 1054 | }); 1055 | return newNode; 1056 | } 1057 | 1058 | export function parse(code: string): Term { 1059 | const node = p.parse(code, { allowInvalidAST: false, loc: true }); 1060 | return convertProgram(node.body); 1061 | } 1062 | 1063 | export function parseArith(code: string): TermForArith { 1064 | return subsetSystem( 1065 | parse(code), 1066 | ["Boolean", "Number"], 1067 | ["true", "false", "if", "number", "add"], 1068 | ); 1069 | } 1070 | 1071 | export function parseBasic(code: string): TermForBasic { 1072 | return subsetSystem( 1073 | parse(code), 1074 | ["Boolean", "Number", "Func"], 1075 | ["true", "false", "if", "number", "add", "var", "func", "call", "seq", "const"], 1076 | ); 1077 | } 1078 | 1079 | export function parseBasic2(code: string): TermForBasic2 { 1080 | function conv(node: Term): Term { 1081 | switch (node.tag) { 1082 | case "if": 1083 | return { tag: "if", cond: conv(node.cond), thn: conv(node.thn), els: conv(node.els), loc: node.loc }; 1084 | case "add": 1085 | return { tag: "add", left: conv(node.left), right: conv(node.right), loc: node.loc }; 1086 | case "func": 1087 | return { tag: "func", params: node.params, body: conv(node.body), loc: node.loc }; 1088 | case "call": 1089 | return { tag: "call", func: conv(node.func), args: node.args.map(conv), loc: node.loc }; 1090 | case "seq": 1091 | case "const": { 1092 | const body: Term[] = []; 1093 | while (true) { 1094 | switch (node.tag) { 1095 | case "seq": { 1096 | body.push(conv(node.body)); 1097 | node = node.rest; 1098 | break; 1099 | } 1100 | case "const": { 1101 | body.push({ tag: "const2", name: node.name, init: conv(node.init), loc: node.loc }); 1102 | node = node.rest; 1103 | break; 1104 | } 1105 | default: { 1106 | body.push(conv(node)); 1107 | return { tag: "seq2", body, loc: node.loc }; 1108 | } 1109 | } 1110 | } 1111 | } 1112 | default: 1113 | return node; 1114 | } 1115 | } 1116 | return subsetSystem( 1117 | conv(parse(code)), 1118 | ["Boolean", "Number", "Func"], 1119 | ["true", "false", "if", "number", "add", "var", "func", "call", "seq2", "const2"], 1120 | ); 1121 | } 1122 | 1123 | export function parseObj(code: string): TermForObj { 1124 | return subsetSystem( 1125 | parse(code), 1126 | ["Boolean", "Number", "Func", "Object"], 1127 | [ 1128 | "true", 1129 | "false", 1130 | "if", 1131 | "number", 1132 | "add", 1133 | "var", 1134 | "func", 1135 | "call", 1136 | "seq", 1137 | "const", 1138 | "objectNew", 1139 | "objectGet", 1140 | ], 1141 | ); 1142 | } 1143 | 1144 | export function parseTaggedUnion(code: string): TermForTaggedUnion { 1145 | return subsetSystem( 1146 | parse(code), 1147 | ["Boolean", "Number", "Func", "TaggedUnion"], 1148 | [ 1149 | "true", 1150 | "false", 1151 | "if", 1152 | "number", 1153 | "add", 1154 | "var", 1155 | "func", 1156 | "call", 1157 | "seq", 1158 | "const", 1159 | "taggedUnionNew", 1160 | "taggedUnionGet", 1161 | ], 1162 | ); 1163 | } 1164 | 1165 | export function parseRecFunc(code: string): TermForRecFunc { 1166 | return subsetSystem( 1167 | parse(code), 1168 | ["Boolean", "Number", "Func"], 1169 | [ 1170 | "true", 1171 | "false", 1172 | "if", 1173 | "number", 1174 | "add", 1175 | "var", 1176 | "func", 1177 | "call", 1178 | "seq", 1179 | "const", 1180 | "recFunc", 1181 | ], 1182 | ); 1183 | } 1184 | 1185 | export function parseSub(code: string): TermForSub { 1186 | return subsetSystem( 1187 | parse(code), 1188 | ["Boolean", "Number", "Func", "Object"], 1189 | ["true", "false", "number", "add", "var", "func", "call", "seq", "const", "objectNew", "objectGet"], 1190 | ); 1191 | } 1192 | 1193 | export function parseRec(code: string): TermForRec { 1194 | return subsetSystem( 1195 | parse(code), 1196 | ["Boolean", "Number", "Func", "Object", "Rec", "TypeVar"], 1197 | [ 1198 | "true", 1199 | "false", 1200 | "if", 1201 | "number", 1202 | "add", 1203 | "var", 1204 | "func", 1205 | "call", 1206 | "seq", 1207 | "const", 1208 | "objectNew", 1209 | "objectGet", 1210 | "recFunc", 1211 | ], 1212 | ); 1213 | } 1214 | 1215 | export function parseRec2(code: string): TermForRec2 { 1216 | return subsetSystem( 1217 | parse(code), 1218 | ["Boolean", "Number", "Func", "Object", "TaggedUnion", "Rec", "TypeVar"], 1219 | [ 1220 | "true", 1221 | "false", 1222 | "if", 1223 | "number", 1224 | "add", 1225 | "var", 1226 | "func", 1227 | "call", 1228 | "seq", 1229 | "const", 1230 | "objectNew", 1231 | "objectGet", 1232 | "taggedUnionNew", 1233 | "taggedUnionGet", 1234 | "recFunc", 1235 | ], 1236 | ); 1237 | } 1238 | 1239 | export function parsePoly(code: string): TermForPoly { 1240 | return subsetSystem( 1241 | parse(code), 1242 | ["Boolean", "Number", "Func", "TypeAbs", "TypeVar"], 1243 | ["true", "false", "if", "number", "add", "var", "func", "call", "seq", "const", "typeAbs", "typeApp"], 1244 | ); 1245 | } 1246 | 1247 | export function parseSelf(code: string): TermForSelf { 1248 | return subsetSystem( 1249 | parse(code), 1250 | ["Boolean", "Number", "String", "Option", "Array", "Record", "Func", "Object", "TaggedUnion", "Rec", "TypeVar"], 1251 | [ 1252 | "true", 1253 | "false", 1254 | "not", 1255 | "compare", 1256 | "if", 1257 | "number", 1258 | "add", 1259 | "string", 1260 | "var", 1261 | "func", 1262 | "call", 1263 | "seq", 1264 | "const", 1265 | "assign", 1266 | "for", 1267 | "forOf", 1268 | "arrayNew", 1269 | "arrayExt", 1270 | "recordNew", 1271 | "recordCopy", 1272 | "recordExt", 1273 | "recordIn", 1274 | "member", 1275 | "objectNew", 1276 | "objectGet", 1277 | "taggedUnionNew", 1278 | "taggedUnionExt", 1279 | "taggedUnionGet", 1280 | "recFunc", 1281 | "undefined", 1282 | ], 1283 | true, 1284 | ); 1285 | } 1286 | 1287 | // --- 1288 | // Helper functions 1289 | 1290 | // deno-lint-ignore no-explicit-any 1291 | export function error(msg: string, node: any): never { 1292 | if (node.loc) { 1293 | const { start, end } = node.loc; 1294 | throw new Error(`test.ts:${start.line}:${start.column + 1}-${end.line}:${end.column + 1} ${msg}`); 1295 | } else { 1296 | throw new Error(msg); 1297 | } 1298 | } 1299 | 1300 | // Returns a string that represents a given type 1301 | export function typeShow(ty: Type): string { 1302 | switch (ty.tag) { 1303 | case "Boolean": 1304 | return "boolean"; 1305 | case "Number": 1306 | return "number"; 1307 | case "String": 1308 | return "string"; 1309 | case "Option": 1310 | return `(${typeShow(ty.elemType)} | undefined)`; 1311 | case "Array": { 1312 | if (ty.elemType.tag === "Func") { 1313 | return `(${typeShow(ty.elemType)})[]`; 1314 | } 1315 | return `${typeShow(ty.elemType)}[]`; 1316 | } 1317 | case "Record": { 1318 | return `Record`; 1319 | } 1320 | case "Func": { 1321 | const params = ty.params.map(({ name, type }) => `${name}: ${typeShow(type)}`); 1322 | return `(${params.join(", ")}) => ${typeShow(ty.retType)}`; 1323 | } 1324 | case "Object": { 1325 | const props = ty.props.map(({ name, type }) => `${name}: ${typeShow(type)}`); 1326 | return `{ ${props.join("; ")} }`; 1327 | } 1328 | case "TaggedUnion": { 1329 | const variants = ty.variants.map(({ tagLabel, props }) => { 1330 | const propsStr = [`tag: "${tagLabel}"`, ...props.map(({ name, type }) => `${name}: ${typeShow(type)}`)]; 1331 | return `{ ${propsStr.join("; ")} }`; 1332 | }); 1333 | return `(${variants.join(" | ")})`; 1334 | } 1335 | case "Rec": 1336 | return `(mu ${ty.name}. ${typeShow(ty.type)})`; 1337 | case "TypeAbs": 1338 | return `<${ty.typeParams.join(", ")}>${typeShow(ty.type)}`; 1339 | case "TypeVar": 1340 | return ty.name; 1341 | case "Undefined": 1342 | return "undefined"; 1343 | } 1344 | } 1345 | -------------------------------------------------------------------------------- /book/typecheckers/arith.ts: -------------------------------------------------------------------------------- 1 | import { error } from "npm:tiny-ts-parser"; 2 | 3 | type Type = 4 | | { tag: "Boolean" } 5 | | { tag: "Number" }; 6 | 7 | type Term = 8 | | { tag: "true" } 9 | | { tag: "false" } 10 | | { tag: "if"; cond: Term; thn: Term; els: Term } 11 | | { tag: "number"; n: number } 12 | | { tag: "add"; left: Term; right: Term }; 13 | 14 | export function typecheck(t: Term): Type { 15 | switch (t.tag) { 16 | case "true": 17 | return { tag: "Boolean" }; 18 | case "false": 19 | return { tag: "Boolean" }; 20 | case "if": { 21 | const condTy = typecheck(t.cond); 22 | if (condTy.tag !== "Boolean") error("boolean expected", t.cond); 23 | const thnTy = typecheck(t.thn); 24 | const elsTy = typecheck(t.els); 25 | if (thnTy.tag !== elsTy.tag) { 26 | error("then and else have different types", t); 27 | } 28 | return thnTy; 29 | } 30 | case "number": 31 | return { tag: "Number" }; 32 | case "add": { 33 | const leftTy = typecheck(t.left); 34 | if (leftTy.tag !== "Number") error("number expected", t.left); 35 | const rightTy = typecheck(t.right); 36 | if (rightTy.tag !== "Number") error("number expected", t.right); 37 | return { tag: "Number" }; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /book/typecheckers/arith_test.ts: -------------------------------------------------------------------------------- 1 | import test from "node:test"; 2 | import assert from "node:assert"; 3 | import { parseArith, typeShow } from "npm:tiny-ts-parser"; 4 | import { typecheck } from "./arith.ts"; 5 | 6 | function run(code: string) { 7 | return typecheck(parseArith(code)); 8 | } 9 | function ok(expected: string, code: string) { 10 | assert.equal(expected, typeShow(run(code))); 11 | } 12 | function ng(expected: RegExp, code: string) { 13 | assert.throws(() => { 14 | run(code); 15 | return true; 16 | }, expected); 17 | } 18 | 19 | test("true", () => ok("boolean", `true`)); 20 | test("false", () => ok("boolean", `false`)); 21 | test("if", () => ok("number", `true ? 1 : 2`)); 22 | test("if error", () => ng(/test.ts:1:1-1:16 then and else have different types/, `true ? 1 : true`)); 23 | 24 | test("number", () => ok("number", `1`)); 25 | test("add", () => ok("number", `1 + 2`)); 26 | test("add error 1", () => ng(/test.ts:1:1-1:5 number expected/, `true + 1`)); 27 | test("add error 2", () => ng(/test.ts:1:5-1:9 number expected/, `1 + true`)); 28 | -------------------------------------------------------------------------------- /book/typecheckers/basic.ts: -------------------------------------------------------------------------------- 1 | import { error } from "npm:tiny-ts-parser"; 2 | 3 | type Type = 4 | | { tag: "Boolean" } 5 | | { tag: "Number" } 6 | | { tag: "Func"; params: Param[]; retType: Type }; 7 | 8 | type Param = { name: string; type: Type }; 9 | 10 | type Term = 11 | | { tag: "true" } 12 | | { tag: "false" } 13 | | { tag: "if"; cond: Term; thn: Term; els: Term } 14 | | { tag: "number"; n: number } 15 | | { tag: "add"; left: Term; right: Term } 16 | | { tag: "var"; name: string } 17 | | { tag: "func"; params: Param[]; body: Term } 18 | | { tag: "call"; func: Term; args: Term[] } 19 | | { tag: "seq"; body: Term; rest: Term } 20 | | { tag: "const"; name: string; init: Term; rest: Term }; 21 | 22 | type TypeEnv = Record; 23 | 24 | function typeEq(ty1: Type, ty2: Type): boolean { 25 | switch (ty2.tag) { 26 | case "Boolean": 27 | return ty1.tag === "Boolean"; 28 | case "Number": 29 | return ty1.tag === "Number"; 30 | case "Func": { 31 | if (ty1.tag !== "Func") return false; 32 | if (ty1.params.length !== ty2.params.length) return false; 33 | for (let i = 0; i < ty1.params.length; i++) { 34 | if (!typeEq(ty1.params[i].type, ty2.params[i].type)) { 35 | return false; 36 | } 37 | } 38 | if (!typeEq(ty1.retType, ty2.retType)) return false; 39 | return true; 40 | } 41 | } 42 | } 43 | 44 | export function typecheck(t: Term, tyEnv: TypeEnv): Type { 45 | switch (t.tag) { 46 | case "true": 47 | return { tag: "Boolean" }; 48 | case "false": 49 | return { tag: "Boolean" }; 50 | case "if": { 51 | const condTy = typecheck(t.cond, tyEnv); 52 | if (condTy.tag !== "Boolean") error("boolean expected", t.cond); 53 | const thnTy = typecheck(t.thn, tyEnv); 54 | const elsTy = typecheck(t.els, tyEnv); 55 | if (!typeEq(thnTy, elsTy)) { 56 | error("then and else have different types", t); 57 | } 58 | return thnTy; 59 | } 60 | case "number": 61 | return { tag: "Number" }; 62 | case "add": { 63 | const leftTy = typecheck(t.left, tyEnv); 64 | if (leftTy.tag !== "Number") error("number expected", t.left); 65 | const rightTy = typecheck(t.right, tyEnv); 66 | if (rightTy.tag !== "Number") error("number expected", t.right); 67 | return { tag: "Number" }; 68 | } 69 | case "var": { 70 | if (tyEnv[t.name] === undefined) error(`unknown variable: ${t.name}`, t); 71 | return tyEnv[t.name]; 72 | } 73 | case "func": { 74 | const newTyEnv = { ...tyEnv }; 75 | for (const { name, type } of t.params) { 76 | newTyEnv[name] = type; 77 | } 78 | const retType = typecheck(t.body, newTyEnv); 79 | return { tag: "Func", params: t.params, retType }; 80 | } 81 | case "call": { 82 | const funcTy = typecheck(t.func, tyEnv); 83 | if (funcTy.tag !== "Func") error("function type expected", t.func); 84 | if (funcTy.params.length !== t.args.length) { 85 | error("wrong number of arguments", t); 86 | } 87 | for (let i = 0; i < t.args.length; i++) { 88 | const argTy = typecheck(t.args[i], tyEnv); 89 | if (!typeEq(argTy, funcTy.params[i].type)) { 90 | error("parameter type mismatch", t.args[i]); 91 | } 92 | } 93 | return funcTy.retType; 94 | } 95 | case "seq": 96 | typecheck(t.body, tyEnv); 97 | return typecheck(t.rest, tyEnv); 98 | case "const": { 99 | const ty = typecheck(t.init, tyEnv); 100 | const newTyEnv = { ...tyEnv, [t.name]: ty }; 101 | return typecheck(t.rest, newTyEnv); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /book/typecheckers/basic2.ts: -------------------------------------------------------------------------------- 1 | import { error } from "npm:tiny-ts-parser"; 2 | 3 | type Type = 4 | | { tag: "Boolean" } 5 | | { tag: "Number" } 6 | | { tag: "Func"; params: Param[]; retType: Type }; 7 | 8 | type Param = { name: string; type: Type }; 9 | 10 | type Term = 11 | | { tag: "true" } 12 | | { tag: "false" } 13 | | { tag: "if"; cond: Term; thn: Term; els: Term } 14 | | { tag: "number"; n: number } 15 | | { tag: "add"; left: Term; right: Term } 16 | | { tag: "var"; name: string } 17 | | { tag: "func"; params: Param[]; body: Term } 18 | | { tag: "call"; func: Term; args: Term[] } 19 | | { tag: "seq2"; body: Term[] } 20 | | { tag: "const2"; name: string; init: Term }; 21 | 22 | type TypeEnv = Record; 23 | 24 | function typeEq(ty1: Type, ty2: Type): boolean { 25 | switch (ty2.tag) { 26 | case "Boolean": 27 | return ty1.tag === "Boolean"; 28 | case "Number": 29 | return ty1.tag === "Number"; 30 | case "Func": { 31 | if (ty1.tag !== "Func") return false; 32 | if (ty1.params.length !== ty2.params.length) return false; 33 | for (let i = 0; i < ty1.params.length; i++) { 34 | if (!typeEq(ty1.params[i].type, ty2.params[i].type)) { 35 | return false; 36 | } 37 | } 38 | if (!typeEq(ty1.retType, ty2.retType)) return false; 39 | return true; 40 | } 41 | } 42 | } 43 | 44 | export function typecheck(t: Term, tyEnv: TypeEnv): Type { 45 | switch (t.tag) { 46 | case "true": 47 | return { tag: "Boolean" }; 48 | case "false": 49 | return { tag: "Boolean" }; 50 | case "if": { 51 | const condTy = typecheck(t.cond, tyEnv); 52 | if (condTy.tag !== "Boolean") error("boolean expected", t.cond); 53 | const thnTy = typecheck(t.thn, tyEnv); 54 | const elsTy = typecheck(t.els, tyEnv); 55 | if (!typeEq(thnTy, elsTy)) { 56 | error("then and else have different types", t); 57 | } 58 | return thnTy; 59 | } 60 | case "number": 61 | return { tag: "Number" }; 62 | case "add": { 63 | const leftTy = typecheck(t.left, tyEnv); 64 | if (leftTy.tag !== "Number") error("number expected", t.left); 65 | const rightTy = typecheck(t.right, tyEnv); 66 | if (rightTy.tag !== "Number") error("number expected", t.right); 67 | return { tag: "Number" }; 68 | } 69 | case "var": { 70 | if (tyEnv[t.name] === undefined) error(`unknown variable: ${t.name}`, t); 71 | return tyEnv[t.name]; 72 | } 73 | case "func": { 74 | const newTyEnv = { ...tyEnv }; 75 | for (const { name, type } of t.params) { 76 | newTyEnv[name] = type; 77 | } 78 | const retType = typecheck(t.body, newTyEnv); 79 | return { tag: "Func", params: t.params, retType }; 80 | } 81 | case "call": { 82 | const funcTy = typecheck(t.func, tyEnv); 83 | if (funcTy.tag !== "Func") error("function type expected", t.func); 84 | if (funcTy.params.length !== t.args.length) { 85 | error("wrong number of arguments", t); 86 | } 87 | for (let i = 0; i < t.args.length; i++) { 88 | const argTy = typecheck(t.args[i], tyEnv); 89 | if (!typeEq(argTy, funcTy.params[i].type)) { 90 | error("parameter type mismatch", t.args[i]); 91 | } 92 | } 93 | return funcTy.retType; 94 | } 95 | case "seq2": { 96 | let lastTy: Type | null = null; 97 | for (const term of t.body) { 98 | if (term.tag === "const2") { 99 | const ty = typecheck(term.init, tyEnv); 100 | tyEnv = { ...tyEnv, [term.name]: ty }; 101 | } else { 102 | lastTy = typecheck(term, tyEnv); 103 | } 104 | } 105 | return lastTy!; 106 | } 107 | case "const2": 108 | throw "unreachable"; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /book/typecheckers/basic2_test.ts: -------------------------------------------------------------------------------- 1 | import test from "node:test"; 2 | import assert from "node:assert"; 3 | import { parseBasic2, typeShow } from "npm:tiny-ts-parser"; 4 | import { typecheck } from "./basic2.ts"; 5 | 6 | function run(code: string) { 7 | return typecheck(parseBasic2(code), {}); 8 | } 9 | function ok(expected: string, code: string) { 10 | assert.equal(expected, typeShow(run(code))); 11 | } 12 | function ng(expected: RegExp, code: string) { 13 | assert.throws(() => { 14 | run(code); 15 | return true; 16 | }, expected); 17 | } 18 | 19 | test("func", () => ok("(x: number, y: number) => boolean", `(x: number, y: number) => true`)); 20 | test("call", () => ok("boolean", `( (x: number, y: number) => true )(1, 2)`)); 21 | test("call error", () => ng(/test.ts:1:40-1:41 parameter type mismatch/, `( (x: number, y: boolean) => true )(1, 2)`)); 22 | test("var", () => ok("number", `((x: number, y: number) => x)(1, 2)`)); 23 | test("var error", () => ng(/test.ts:1:28-1:29 unknown variable: z/, `((x: number, y: number) => z)(1, 2)`)); 24 | test("func func", () => ok("(f: (x: number) => number) => number", `( (f: (x: number) => number) => f(42) )`)); 25 | 26 | test("seq 1", () => ok("number", `1; 2; 3`)); 27 | test("seq 2", () => ok("boolean", `1; 2; true`)); 28 | test("const 1", () => 29 | ok( 30 | "number", 31 | ` 32 | const x = 1; 33 | const y = 2; 34 | x + y; 35 | `, 36 | )); 37 | 38 | test("const 2", () => 39 | ok( 40 | "number", 41 | ` 42 | const f = (x: number, y: number) => x + y; 43 | f(1, 2); 44 | `, 45 | )); 46 | test("const error 1", () => 47 | ng( 48 | /./, 49 | ` 50 | const f = (x: number, y: number) => x + y; 51 | f(1, true); 52 | `, 53 | )); 54 | test("const error 2", () => 55 | ng( 56 | /./, 57 | ` 58 | const fib = (n: number) => { 59 | return fib(n + 1) + fib(n); 60 | }; 61 | fib(1) 62 | `, 63 | )); 64 | -------------------------------------------------------------------------------- /book/typecheckers/basic_test.ts: -------------------------------------------------------------------------------- 1 | import test from "node:test"; 2 | import assert from "node:assert"; 3 | import { parseBasic, typeShow } from "npm:tiny-ts-parser"; 4 | import { typecheck } from "./basic.ts"; 5 | 6 | function run(code: string) { 7 | return typecheck(parseBasic(code), {}); 8 | } 9 | function ok(expected: string, code: string) { 10 | assert.equal(expected, typeShow(run(code))); 11 | } 12 | function ng(expected: RegExp, code: string) { 13 | assert.throws(() => { 14 | run(code); 15 | return true; 16 | }, expected); 17 | } 18 | 19 | test("func", () => ok("(x: number, y: number) => boolean", `(x: number, y: number) => true`)); 20 | test("call", () => ok("boolean", `( (x: number, y: number) => true )(1, 2)`)); 21 | test("call error", () => ng(/test.ts:1:40-1:41 parameter type mismatch/, `( (x: number, y: boolean) => true )(1, 2)`)); 22 | test("var", () => ok("number", `((x: number, y: number) => x)(1, 2)`)); 23 | test("var error", () => ng(/test.ts:1:28-1:29 unknown variable: z/, `((x: number, y: number) => z)(1, 2)`)); 24 | test("func func", () => ok("(f: (x: number) => number) => number", `( (f: (x: number) => number) => f(42) )`)); 25 | 26 | test("seq 1", () => ok("number", `1; 2; 3`)); 27 | test("seq 2", () => ok("boolean", `1; 2; true`)); 28 | test("const 1", () => 29 | ok( 30 | "number", 31 | ` 32 | const x = 1; 33 | const y = 2; 34 | x + y; 35 | `, 36 | )); 37 | 38 | test("const 2", () => 39 | ok( 40 | "number", 41 | ` 42 | const f = (x: number, y: number) => x + y; 43 | f(1, 2); 44 | `, 45 | )); 46 | test("const error 1", () => 47 | ng( 48 | /./, 49 | ` 50 | const f = (x: number, y: number) => x + y; 51 | f(1, true); 52 | `, 53 | )); 54 | test("const error 2", () => 55 | ng( 56 | /./, 57 | ` 58 | const fib = (n: number) => { 59 | return fib(n + 1) + fib(n); 60 | }; 61 | fib(1) 62 | `, 63 | )); 64 | -------------------------------------------------------------------------------- /book/typecheckers/obj.ts: -------------------------------------------------------------------------------- 1 | import { error } from "npm:tiny-ts-parser"; 2 | 3 | type Type = 4 | | { tag: "Boolean" } 5 | | { tag: "Number" } 6 | | { tag: "Func"; params: Param[]; retType: Type } 7 | | { tag: "Object"; props: PropertyType[] }; 8 | 9 | type Param = { name: string; type: Type }; 10 | type PropertyType = { name: string; type: Type }; 11 | 12 | type Term = 13 | | { tag: "true" } 14 | | { tag: "false" } 15 | | { tag: "if"; cond: Term; thn: Term; els: Term } 16 | | { tag: "number"; n: number } 17 | | { tag: "add"; left: Term; right: Term } 18 | | { tag: "var"; name: string } 19 | | { tag: "func"; params: Param[]; body: Term } 20 | | { tag: "call"; func: Term; args: Term[] } 21 | | { tag: "seq"; body: Term; rest: Term } 22 | | { tag: "const"; name: string; init: Term; rest: Term } 23 | | { tag: "objectNew"; props: PropertyTerm[] } 24 | | { tag: "objectGet"; obj: Term; propName: string }; 25 | 26 | type PropertyTerm = { name: string; term: Term }; 27 | 28 | type TypeEnv = Record; 29 | 30 | function typeEq(ty1: Type, ty2: Type): boolean { 31 | switch (ty2.tag) { 32 | case "Boolean": 33 | return ty1.tag === "Boolean"; 34 | case "Number": 35 | return ty1.tag === "Number"; 36 | case "Func": { 37 | if (ty1.tag !== "Func") return false; 38 | if (ty1.params.length !== ty2.params.length) return false; 39 | for (let i = 0; i < ty1.params.length; i++) { 40 | if (!typeEq(ty1.params[i].type, ty2.params[i].type)) { 41 | return false; 42 | } 43 | } 44 | if (!typeEq(ty1.retType, ty2.retType)) return false; 45 | return true; 46 | } 47 | case "Object": { 48 | if (ty1.tag !== "Object") return false; 49 | if (ty1.props.length !== ty2.props.length) return false; 50 | for (const prop2 of ty2.props) { 51 | const prop1 = ty1.props.find((prop1) => prop1.name === prop2.name); 52 | if (!prop1) return false; 53 | if (!typeEq(prop1.type, prop2.type)) return false; 54 | } 55 | return true; 56 | } 57 | } 58 | } 59 | 60 | export function typecheck(t: Term, tyEnv: TypeEnv): Type { 61 | switch (t.tag) { 62 | case "true": 63 | return { tag: "Boolean" }; 64 | case "false": 65 | return { tag: "Boolean" }; 66 | case "if": { 67 | const condTy = typecheck(t.cond, tyEnv); 68 | if (condTy.tag !== "Boolean") error("boolean expected", t.cond); 69 | const thnTy = typecheck(t.thn, tyEnv); 70 | const elsTy = typecheck(t.els, tyEnv); 71 | if (!typeEq(thnTy, elsTy)) { 72 | error("then and else have different types", t); 73 | } 74 | return thnTy; 75 | } 76 | case "number": 77 | return { tag: "Number" }; 78 | case "add": { 79 | const leftTy = typecheck(t.left, tyEnv); 80 | if (leftTy.tag !== "Number") error("number expected", t.left); 81 | const rightTy = typecheck(t.right, tyEnv); 82 | if (rightTy.tag !== "Number") error("number expected", t.right); 83 | return { tag: "Number" }; 84 | } 85 | case "var": { 86 | if (tyEnv[t.name] === undefined) error(`unknown variable: ${t.name}`, t); 87 | return tyEnv[t.name]; 88 | } 89 | case "func": { 90 | const newTyEnv = { ...tyEnv }; 91 | for (const { name, type } of t.params) { 92 | newTyEnv[name] = type; 93 | } 94 | const retType = typecheck(t.body, newTyEnv); 95 | return { tag: "Func", params: t.params, retType }; 96 | } 97 | case "call": { 98 | const funcTy = typecheck(t.func, tyEnv); 99 | if (funcTy.tag !== "Func") error("function type expected", t.func); 100 | if (funcTy.params.length !== t.args.length) { 101 | error("wrong number of arguments", t); 102 | } 103 | for (let i = 0; i < t.args.length; i++) { 104 | const argTy = typecheck(t.args[i], tyEnv); 105 | if (!typeEq(argTy, funcTy.params[i].type)) { 106 | error("parameter type mismatch", t.args[i]); 107 | } 108 | } 109 | return funcTy.retType; 110 | } 111 | case "seq": 112 | typecheck(t.body, tyEnv); 113 | return typecheck(t.rest, tyEnv); 114 | case "const": { 115 | const ty = typecheck(t.init, tyEnv); 116 | const newTyEnv = { ...tyEnv, [t.name]: ty }; 117 | return typecheck(t.rest, newTyEnv); 118 | } 119 | case "objectNew": { 120 | const props = t.props.map( 121 | ({ name, term }) => ({ name, type: typecheck(term, tyEnv) }), 122 | ); 123 | return { tag: "Object", props }; 124 | } 125 | case "objectGet": { 126 | const objectTy = typecheck(t.obj, tyEnv); 127 | if (objectTy.tag !== "Object") error("object type expected", t.obj); 128 | const prop = objectTy.props.find((prop) => prop.name === t.propName); 129 | if (!prop) error(`unknown property name: ${t.propName}`, t); 130 | return prop.type; 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /book/typecheckers/obj_test.ts: -------------------------------------------------------------------------------- 1 | import test from "node:test"; 2 | import assert from "node:assert"; 3 | import { parseObj, typeShow } from "npm:tiny-ts-parser"; 4 | import { typecheck } from "./obj.ts"; 5 | 6 | function run(code: string) { 7 | return typecheck(parseObj(code), {}); 8 | } 9 | function ok(expected: string, code: string) { 10 | assert.equal(expected, typeShow(run(code))); 11 | } 12 | function ng(expected: RegExp, code: string) { 13 | assert.throws(() => { 14 | run(code); 15 | return true; 16 | }, expected); 17 | } 18 | 19 | test("object 1", () => ok("{ a: number; b: boolean }", `({ a: 1, b: true });`)); 20 | test("object 2", () => 21 | ok( 22 | "number", 23 | ` 24 | const obj = { a: 1, b: true }; 25 | obj.a; 26 | `, 27 | )); 28 | test("object 3", () => 29 | ok( 30 | "boolean", 31 | ` 32 | const obj = { a: 1, b: true }; 33 | obj.b; 34 | `, 35 | )); 36 | test("object error 1", () => 37 | ng( 38 | /test.ts:3:3-3:8 unknown property name: c/, 39 | ` 40 | const obj = { a: 1, b: true }; 41 | obj.c; 42 | `, 43 | )); 44 | test("object error 2", () => 45 | ng( 46 | /test.ts:3:5-3:8 object type expected/, 47 | ` 48 | const obj = 42; 49 | obj.b; 50 | `, 51 | )); 52 | -------------------------------------------------------------------------------- /book/typecheckers/poly.ts: -------------------------------------------------------------------------------- 1 | import { error } from "npm:tiny-ts-parser"; 2 | 3 | type Type = 4 | | { tag: "Boolean" } 5 | | { tag: "Number" } 6 | | { tag: "Func"; params: Param[]; retType: Type } 7 | | { tag: "TypeAbs"; typeParams: string[]; type: Type } 8 | | { tag: "TypeVar"; name: string }; 9 | 10 | type Param = { name: string; type: Type }; 11 | 12 | type Term = 13 | | { tag: "true" } 14 | | { tag: "false" } 15 | | { tag: "if"; cond: Term; thn: Term; els: Term } 16 | | { tag: "number"; n: number } 17 | | { tag: "add"; left: Term; right: Term } 18 | | { tag: "var"; name: string } 19 | | { tag: "func"; params: Param[]; body: Term } 20 | | { tag: "call"; func: Term; args: Term[] } 21 | | { tag: "seq"; body: Term; rest: Term } 22 | | { tag: "const"; name: string; init: Term; rest: Term } 23 | | { tag: "typeAbs"; typeParams: string[]; body: Term } 24 | | { tag: "typeApp"; typeAbs: Term; typeArgs: Type[] }; 25 | 26 | type TypeEnv = Record; 27 | 28 | let freshTyVarId = 1; 29 | 30 | function freshTypeAbs(typeParams: string[], ty: Type) { 31 | let newType = ty; 32 | const newTypeParams = []; 33 | for (const tyVar of typeParams) { 34 | const newTyVar = `${tyVar}@${freshTyVarId++}`; 35 | newType = subst(newType, tyVar, { tag: "TypeVar", name: newTyVar }); 36 | newTypeParams.push(newTyVar); 37 | } 38 | return { newTypeParams, newType }; 39 | } 40 | 41 | function subst(ty: Type, tyVarName: string, repTy: Type): Type { 42 | switch (ty.tag) { 43 | case "Boolean": 44 | case "Number": 45 | return ty; 46 | case "Func": { 47 | const params = ty.params.map( 48 | ({ name, type }) => ({ name, type: subst(type, tyVarName, repTy) }), 49 | ); 50 | const retType = subst(ty.retType, tyVarName, repTy); 51 | return { tag: "Func", params, retType }; 52 | } 53 | case "TypeAbs": { 54 | if (ty.typeParams.includes(tyVarName)) return ty; 55 | const { newTypeParams, newType } = freshTypeAbs(ty.typeParams, ty.type); 56 | const newType2 = subst(newType, tyVarName, repTy); 57 | return { tag: "TypeAbs", typeParams: newTypeParams, type: newType2 }; 58 | } 59 | case "TypeVar": { 60 | return ty.name === tyVarName ? repTy : ty; 61 | } 62 | } 63 | } 64 | 65 | function typeEqSub(ty1: Type, ty2: Type, map: Record): boolean { 66 | switch (ty2.tag) { 67 | case "Boolean": 68 | return ty1.tag === "Boolean"; 69 | case "Number": 70 | return ty1.tag === "Number"; 71 | case "Func": { 72 | if (ty1.tag !== "Func") return false; 73 | if (ty1.params.length !== ty2.params.length) return false; 74 | for (let i = 0; i < ty1.params.length; i++) { 75 | if (!typeEqSub(ty1.params[i].type, ty2.params[i].type, map)) { 76 | return false; 77 | } 78 | } 79 | if (!typeEqSub(ty1.retType, ty2.retType, map)) return false; 80 | return true; 81 | } 82 | case "TypeAbs": { 83 | if (ty1.tag !== "TypeAbs") return false; 84 | if (ty1.typeParams.length !== ty2.typeParams.length) return false; 85 | const newMap = { ...map }; 86 | for (let i = 0; i < ty1.typeParams.length; i++) { 87 | newMap[ty1.typeParams[i]] = ty2.typeParams[i]; 88 | } 89 | return typeEqSub(ty1.type, ty2.type, newMap); 90 | } 91 | case "TypeVar": { 92 | if (ty1.tag !== "TypeVar") return false; 93 | if (map[ty1.name] === undefined) { 94 | throw new Error(`unknown type variable: ${ty1.name}`); 95 | } 96 | return map[ty1.name] === ty2.name; 97 | } 98 | } 99 | } 100 | 101 | function typeEq(ty1: Type, ty2: Type, tyVars: string[]): boolean { 102 | const map: Record = {}; 103 | for (const tyVar of tyVars) map[tyVar] = tyVar; 104 | return typeEqSub(ty1, ty2, map); 105 | } 106 | 107 | export function typecheck(t: Term, tyEnv: TypeEnv, tyVars: string[]): Type { 108 | switch (t.tag) { 109 | case "true": 110 | return { tag: "Boolean" }; 111 | case "false": 112 | return { tag: "Boolean" }; 113 | case "if": { 114 | const condTy = typecheck(t.cond, tyEnv, tyVars); 115 | if (condTy.tag !== "Boolean") error("boolean expected", t.cond); 116 | const thnTy = typecheck(t.thn, tyEnv, tyVars); 117 | const elsTy = typecheck(t.els, tyEnv, tyVars); 118 | if (!typeEq(thnTy, elsTy, tyVars)) { 119 | error("then and else have different types", t); 120 | } 121 | return thnTy; 122 | } 123 | case "number": 124 | return { tag: "Number" }; 125 | case "add": { 126 | const leftTy = typecheck(t.left, tyEnv, tyVars); 127 | if (leftTy.tag !== "Number") error("number expected", t.left); 128 | const rightTy = typecheck(t.right, tyEnv, tyVars); 129 | if (rightTy.tag !== "Number") error("number expected", t.right); 130 | return { tag: "Number" }; 131 | } 132 | case "var": { 133 | if (tyEnv[t.name] === undefined) error(`unknown variable: ${t.name}`, t); 134 | return tyEnv[t.name]; 135 | } 136 | case "func": { 137 | const newTyEnv = { ...tyEnv }; 138 | for (const { name, type } of t.params) { 139 | newTyEnv[name] = type; 140 | } 141 | const retType = typecheck(t.body, newTyEnv, tyVars); 142 | return { tag: "Func", params: t.params, retType }; 143 | } 144 | case "call": { 145 | const funcTy = typecheck(t.func, tyEnv, tyVars); 146 | if (funcTy.tag !== "Func") error("function type expected", t.func); 147 | if (funcTy.params.length !== t.args.length) { 148 | error("wrong number of arguments", t); 149 | } 150 | for (let i = 0; i < t.args.length; i++) { 151 | const argTy = typecheck(t.args[i], tyEnv, tyVars); 152 | if (!typeEq(argTy, funcTy.params[i].type, tyVars)) { 153 | error("parameter type mismatch", t.args[i]); 154 | } 155 | } 156 | return funcTy.retType; 157 | } 158 | case "seq": 159 | typecheck(t.body, tyEnv, tyVars); 160 | return typecheck(t.rest, tyEnv, tyVars); 161 | case "const": { 162 | const ty = typecheck(t.init, tyEnv, tyVars); 163 | const newTyEnv = { ...tyEnv, [t.name]: ty }; 164 | return typecheck(t.rest, newTyEnv, tyVars); 165 | } 166 | case "typeAbs": { 167 | const tyVars2 = [...tyVars]; 168 | for (const tyVar of t.typeParams) tyVars2.push(tyVar); 169 | const bodyTy = typecheck(t.body, tyEnv, tyVars2); 170 | return { tag: "TypeAbs", typeParams: t.typeParams, type: bodyTy }; 171 | } 172 | case "typeApp": { 173 | const bodyTy = typecheck(t.typeAbs, tyEnv, tyVars); 174 | if (bodyTy.tag !== "TypeAbs") { 175 | error("type abstraction expected", t.typeAbs); 176 | } 177 | if (bodyTy.typeParams.length !== t.typeArgs.length) { 178 | error("wrong number of type arguments", t); 179 | } 180 | let newTy = bodyTy.type; 181 | for (let i = 0; i < bodyTy.typeParams.length; i++) { 182 | newTy = subst(newTy, bodyTy.typeParams[i], t.typeArgs[i]); 183 | } 184 | return newTy; 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /book/typecheckers/poly_bug.ts: -------------------------------------------------------------------------------- 1 | import { error } from "npm:tiny-ts-parser"; 2 | 3 | type Type = 4 | | { tag: "Boolean" } 5 | | { tag: "Number" } 6 | | { tag: "Func"; params: Param[]; retType: Type } 7 | | { tag: "TypeAbs"; typeParams: string[]; type: Type } 8 | | { tag: "TypeVar"; name: string }; 9 | 10 | type Param = { name: string; type: Type }; 11 | 12 | type Term = 13 | | { tag: "true" } 14 | | { tag: "false" } 15 | | { tag: "if"; cond: Term; thn: Term; els: Term } 16 | | { tag: "number"; n: number } 17 | | { tag: "add"; left: Term; right: Term } 18 | | { tag: "var"; name: string } 19 | | { tag: "func"; params: Param[]; body: Term } 20 | | { tag: "call"; func: Term; args: Term[] } 21 | | { tag: "seq"; body: Term; rest: Term } 22 | | { tag: "const"; name: string; init: Term; rest: Term } 23 | | { tag: "typeAbs"; typeParams: string[]; body: Term } 24 | | { tag: "typeApp"; typeAbs: Term; typeArgs: Type[] }; 25 | 26 | type TypeEnv = Record; 27 | 28 | function subst(ty: Type, tyVarName: string, repTy: Type): Type { 29 | switch (ty.tag) { 30 | case "Boolean": 31 | case "Number": 32 | return ty; 33 | case "Func": { 34 | const params = ty.params.map( 35 | ({ name, type }) => ({ name, type: subst(type, tyVarName, repTy) }), 36 | ); 37 | const retType = subst(ty.retType, tyVarName, repTy); 38 | return { tag: "Func", params, retType }; 39 | } 40 | case "TypeAbs": { 41 | //if (ty.typeParams.includes(tyVarName)) return ty; 42 | const newType = subst(ty.type, tyVarName, repTy); 43 | return { tag: "TypeAbs", typeParams: ty.typeParams, type: newType }; 44 | } 45 | case "TypeVar": { 46 | return ty.name === tyVarName ? repTy : ty; 47 | } 48 | } 49 | } 50 | 51 | function typeEqSub(ty1: Type, ty2: Type, map: Record): boolean { 52 | switch (ty2.tag) { 53 | case "Boolean": 54 | return ty1.tag === "Boolean"; 55 | case "Number": 56 | return ty1.tag === "Number"; 57 | case "Func": { 58 | if (ty1.tag !== "Func") return false; 59 | if (ty1.params.length !== ty2.params.length) return false; 60 | for (let i = 0; i < ty1.params.length; i++) { 61 | if (!typeEqSub(ty1.params[i].type, ty2.params[i].type, map)) { 62 | return false; 63 | } 64 | } 65 | if (!typeEqSub(ty1.retType, ty2.retType, map)) return false; 66 | return true; 67 | } 68 | case "TypeAbs": { 69 | if (ty1.tag !== "TypeAbs") return false; 70 | if (ty1.typeParams.length !== ty2.typeParams.length) return false; 71 | const newMap = { ...map }; 72 | for (let i = 0; i < ty1.typeParams.length; i++) { 73 | newMap[ty1.typeParams[i]] = ty2.typeParams[i]; 74 | } 75 | return typeEqSub(ty1.type, ty2.type, newMap); 76 | } 77 | case "TypeVar": { 78 | if (ty1.tag !== "TypeVar") return false; 79 | if (map[ty1.name] === undefined) { 80 | throw new Error(`unknown type variable: ${ty1.name}`); 81 | } 82 | return map[ty1.name] === ty2.name; 83 | } 84 | } 85 | } 86 | 87 | function typeEq(ty1: Type, ty2: Type, tyVars: string[]): boolean { 88 | const map: Record = {}; 89 | for (const tyVar of tyVars) map[tyVar] = tyVar; 90 | return typeEqSub(ty1, ty2, map); 91 | } 92 | 93 | export function typecheck(t: Term, tyEnv: TypeEnv, tyVars: string[]): Type { 94 | switch (t.tag) { 95 | case "true": 96 | return { tag: "Boolean" }; 97 | case "false": 98 | return { tag: "Boolean" }; 99 | case "if": { 100 | const condTy = typecheck(t.cond, tyEnv, tyVars); 101 | if (condTy.tag !== "Boolean") error("boolean expected", t.cond); 102 | const thnTy = typecheck(t.thn, tyEnv, tyVars); 103 | const elsTy = typecheck(t.els, tyEnv, tyVars); 104 | if (!typeEq(thnTy, elsTy, tyVars)) { 105 | error("then and else have different types", t); 106 | } 107 | return thnTy; 108 | } 109 | case "number": 110 | return { tag: "Number" }; 111 | case "add": { 112 | const leftTy = typecheck(t.left, tyEnv, tyVars); 113 | if (leftTy.tag !== "Number") error("number expected", t.left); 114 | const rightTy = typecheck(t.right, tyEnv, tyVars); 115 | if (rightTy.tag !== "Number") error("number expected", t.right); 116 | return { tag: "Number" }; 117 | } 118 | case "var": { 119 | if (tyEnv[t.name] === undefined) error(`unknown variable: ${t.name}`, t); 120 | return tyEnv[t.name]; 121 | } 122 | case "func": { 123 | const newTyEnv = { ...tyEnv }; 124 | for (const { name, type } of t.params) { 125 | newTyEnv[name] = type; 126 | } 127 | const retType = typecheck(t.body, newTyEnv, tyVars); 128 | return { tag: "Func", params: t.params, retType }; 129 | } 130 | case "call": { 131 | const funcTy = typecheck(t.func, tyEnv, tyVars); 132 | if (funcTy.tag !== "Func") error("function type expected", t.func); 133 | if (funcTy.params.length !== t.args.length) { 134 | error("wrong number of arguments", t); 135 | } 136 | for (let i = 0; i < t.args.length; i++) { 137 | const argTy = typecheck(t.args[i], tyEnv, tyVars); 138 | if (!typeEq(argTy, funcTy.params[i].type, tyVars)) { 139 | error("parameter type mismatch", t.args[i]); 140 | } 141 | } 142 | return funcTy.retType; 143 | } 144 | case "seq": 145 | typecheck(t.body, tyEnv, tyVars); 146 | return typecheck(t.rest, tyEnv, tyVars); 147 | case "const": { 148 | const ty = typecheck(t.init, tyEnv, tyVars); 149 | const newTyEnv = { ...tyEnv, [t.name]: ty }; 150 | return typecheck(t.rest, newTyEnv, tyVars); 151 | } 152 | case "typeAbs": { 153 | const tyVars2 = [...tyVars]; 154 | for (const tyVar of t.typeParams) tyVars2.push(tyVar); 155 | const bodyTy = typecheck(t.body, tyEnv, tyVars2); 156 | return { tag: "TypeAbs", typeParams: t.typeParams, type: bodyTy }; 157 | } 158 | case "typeApp": { 159 | const bodyTy = typecheck(t.typeAbs, tyEnv, tyVars); 160 | if (bodyTy.tag !== "TypeAbs") { 161 | error("type abstraction expected", t.typeAbs); 162 | } 163 | if (bodyTy.typeParams.length !== t.typeArgs.length) { 164 | error("wrong number of type arguments", t); 165 | } 166 | let newTy = bodyTy.type; 167 | for (let i = 0; i < bodyTy.typeParams.length; i++) { 168 | newTy = subst(newTy, bodyTy.typeParams[i], t.typeArgs[i]); 169 | } 170 | return newTy; 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /book/typecheckers/poly_test.ts: -------------------------------------------------------------------------------- 1 | import test from "node:test"; 2 | import assert from "node:assert"; 3 | import { parsePoly, typeShow } from "npm:tiny-ts-parser"; 4 | import { typecheck } from "./poly.ts"; 5 | 6 | function run(code: string) { 7 | return typecheck(parsePoly(code), {}, []); 8 | } 9 | function ok(expected: string, code: string) { 10 | assert.equal(expected, typeShow(run(code))); 11 | } 12 | function ng(expected: RegExp, code: string) { 13 | assert.throws(() => { 14 | run(code); 15 | return true; 16 | }, expected); 17 | } 18 | 19 | test("test 1", () => 20 | ok( 21 | "number", 22 | ` 23 | const f = (x: X) => x; 24 | const g = (x: Y) => f(x); 25 | g(1); 26 | `, 27 | )); 28 | test("lower tyvar name", () => 29 | ok( 30 | "number", 31 | ` 32 | const g = (x: x) => x; 33 | const f = (x: x) => g(x); 34 | const r = f(0); 35 | r 36 | `, 37 | )); 38 | test("tyvar capture", () => 39 | ok( 40 | "(_x: X) => (f: (z: X@1) => X) => X", 41 | ` 42 | const g = (f: (z: X) => Y) => f(true); 43 | const f = (_x: X) => g; 44 | f; 45 | `, 46 | )); 47 | test("test 2", () => 48 | ok( 49 | "(x: Z@3) => Z@3", 50 | ` 51 | const f = (x: (x: Z) => Z) => x; 52 | const g = (x: Y) => x; 53 | f<(x: W) => W>(g); 54 | `, 55 | )); 56 | test("type abs 1", () => ok("(x: X) => X", `(x: X) => x`)); 57 | test("type abs 2", () => ok("(f: (x: X) => X) => number", `(f: (x: X) => X) => 1`)); 58 | test("type abs 3", () => 59 | ok( 60 | "(f: (x: X) => boolean) => number", 61 | ` 62 | type F = (x: X) => G; 63 | type G = X; 64 | type X = boolean; 65 | (f: F) => 1; 66 | `, 67 | )); 68 | test("type abs 4", () => 69 | ok( 70 | "number", 71 | ` 72 | const f = () => { 73 | return (x: X) => x; 74 | } 75 | f()(1) 76 | `, 77 | )); 78 | test("type app 1", () => 79 | ok( 80 | "(x: number) => number", 81 | ` 82 | const f = (x: X) => x; 83 | f; 84 | `, 85 | )); 86 | test("type app 2", () => 87 | ok( 88 | "number", 89 | ` 90 | const f = (x: X) => x; 91 | f(0); 92 | `, 93 | )); 94 | 95 | test("type app 3", () => 96 | ok( 97 | "boolean", 98 | ` 99 | const f = (x: X) => x; 100 | f(true); 101 | `, 102 | )); 103 | test("type app 4", () => 104 | ok( 105 | "number", 106 | ` 107 | const f = (f: (x: X) => X) => 1; 108 | const g = (y: Y) => y; 109 | f(g); 110 | `, 111 | )); 112 | test("type app 5", () => 113 | ng( 114 | /test.ts:4:5-4:6 parameter type mismatch/, 115 | ` 116 | const f = (f: (x: X) => X) => 1; 117 | const g = (y: Y) => 1; 118 | f(g); 119 | `, 120 | )); 121 | test("type param 1", () => 122 | ok( 123 | "(f: (x: number) => boolean) => number", 124 | ` 125 | type F = (x: X) => boolean; 126 | (f: F) => 1; 127 | `, 128 | )); 129 | test("type param 2", () => 130 | ok( 131 | "(f: { b: { a: boolean } }) => number", 132 | ` 133 | type F = G<{a: X}>; 134 | type G = {b: X}; 135 | (f: F) => 1; 136 | `, 137 | )); 138 | test("type param 3", () => 139 | ok( 140 | "(f: { a: boolean; b: number }) => number", 141 | ` 142 | type F = {a: X, b: Y}; 143 | (f: F) => 1; 144 | `, 145 | )); 146 | test("type param error 1", () => 147 | ng( 148 | /test.ts:2:15-2:24 not a generic type: X/, 149 | ` 150 | type F = X; 151 | (f: F) => 1; 152 | `, 153 | )); 154 | test("type param error 2", () => 155 | ng( 156 | /test.ts:2:25-2:34 type recursion for generics is not supported/, 157 | ` 158 | type F = (x: X) => F; 159 | (f: F) => 1; 160 | `, 161 | )); 162 | test("type param error 3", () => 163 | ng( 164 | /test.ts:2:15-2:24 unbound type variable: G/, 165 | ` 166 | type F = G; 167 | (f: F) => 1; 168 | `, 169 | )); 170 | test("type param error 4", () => 171 | ng( 172 | /test.ts:2:15-2:24 not a generic type: G/, 173 | ` 174 | type F = G; 175 | type G = () => X; 176 | (f: F) => 1; 177 | `, 178 | )); 179 | test("type param error 5", () => 180 | ng( 181 | /test.ts:2:15-2:32 not a generic type: G/, 182 | ` 183 | type F = G; 184 | type G = () => X; 185 | (f: F) => 1; 186 | `, 187 | )); 188 | test("type param error 6", () => 189 | ng( 190 | /test.ts:2:15-2:16 unbound type variable: G/, 191 | ` 192 | type F = G; 193 | (f: F) => 1; 194 | `, 195 | )); 196 | test("type param error 7", () => 197 | ng( 198 | /test.ts:2:15-2:16 type arguments are required for G/, 199 | ` 200 | type F = G; 201 | type G = number; 202 | (f: F) => 1; 203 | `, 204 | )); 205 | 206 | test("type application to non var", () => 207 | ok( 208 | "number", 209 | ` 210 | const f = (x: X) => x; 211 | const g = () => f; 212 | g()(1); 213 | `, 214 | )); -------------------------------------------------------------------------------- /book/typecheckers/rec.ts: -------------------------------------------------------------------------------- 1 | import { error } from "npm:tiny-ts-parser"; 2 | 3 | type Type = 4 | | { tag: "Boolean" } 5 | | { tag: "Number" } 6 | | { tag: "Func"; params: Param[]; retType: Type } 7 | | { tag: "Object"; props: PropertyType[] } 8 | | { tag: "Rec"; name: string; type: Type } 9 | | { tag: "TypeVar"; name: string }; 10 | 11 | type Param = { name: string; type: Type }; 12 | type PropertyType = { name: string; type: Type }; 13 | 14 | type Term = 15 | | { tag: "true" } 16 | | { tag: "false" } 17 | | { tag: "if"; cond: Term; thn: Term; els: Term } 18 | | { tag: "number"; n: number } 19 | | { tag: "add"; left: Term; right: Term } 20 | | { tag: "var"; name: string } 21 | | { tag: "func"; params: Param[]; body: Term } 22 | | { tag: "call"; func: Term; args: Term[] } 23 | | { tag: "seq"; body: Term; rest: Term } 24 | | { tag: "const"; name: string; init: Term; rest: Term } 25 | | { tag: "objectNew"; props: PropertyTerm[] } 26 | | { tag: "objectGet"; obj: Term; propName: string } 27 | | { 28 | tag: "recFunc"; 29 | funcName: string; 30 | params: Param[]; 31 | retType: Type; 32 | body: Term; 33 | rest: Term; 34 | }; 35 | 36 | type PropertyTerm = { name: string; term: Term }; 37 | 38 | type TypeEnv = Record; 39 | 40 | function typeEqNaive(ty1: Type, ty2: Type, map: Record): boolean { 41 | switch (ty2.tag) { 42 | case "Boolean": 43 | case "Number": 44 | return ty1.tag === ty2.tag; 45 | case "Func": { 46 | if (ty1.tag !== "Func") return false; 47 | for (let i = 0; i < ty1.params.length; i++) { 48 | if (!typeEqNaive(ty1.params[i].type, ty2.params[i].type, map)) { 49 | return false; 50 | } 51 | } 52 | if (!typeEqNaive(ty1.retType, ty2.retType, map)) return false; 53 | return true; 54 | } 55 | case "Object": { 56 | if (ty1.tag !== "Object") return false; 57 | if (ty1.props.length !== ty2.props.length) return false; 58 | for (const prop1 of ty1.props) { 59 | const prop2 = ty2.props.find((prop2) => prop1.name === prop2.name); 60 | if (!prop2) return false; 61 | if (!typeEqNaive(prop1.type, prop2.type, map)) return false; 62 | } 63 | return true; 64 | } 65 | case "Rec": { 66 | if (ty1.tag !== "Rec") return false; 67 | const newMap = { ...map, [ty1.name]: ty2.name }; 68 | return typeEqNaive(ty1.type, ty2.type, newMap); 69 | } 70 | case "TypeVar": { 71 | if (ty1.tag !== "TypeVar") return false; 72 | if (map[ty1.name] === undefined) { 73 | throw new Error(`unknown type variable: ${ty1.name}`); 74 | } 75 | return map[ty1.name] === ty2.name; 76 | } 77 | } 78 | } 79 | 80 | function expandType(ty: Type, tyVarName: string, repTy: Type): Type { 81 | switch (ty.tag) { 82 | case "Boolean": 83 | case "Number": 84 | return ty; 85 | case "Func": { 86 | const params = ty.params.map( 87 | ({ name, type }) => ({ name, type: expandType(type, tyVarName, repTy) }), 88 | ); 89 | const retType = expandType(ty.retType, tyVarName, repTy); 90 | return { tag: "Func", params, retType }; 91 | } 92 | case "Object": { 93 | const props = ty.props.map( 94 | ({ name, type }) => ({ name, type: expandType(type, tyVarName, repTy) }), 95 | ); 96 | return { tag: "Object", props }; 97 | } 98 | case "Rec": { 99 | if (ty.name === tyVarName) return ty; 100 | const newType = expandType(ty.type, tyVarName, repTy); 101 | return { tag: "Rec", name: ty.name, type: newType }; 102 | } 103 | case "TypeVar": { 104 | return ty.name === tyVarName ? repTy : ty; 105 | } 106 | } 107 | } 108 | 109 | function simplifyType(ty: Type): Type { 110 | switch (ty.tag) { 111 | case "Rec": 112 | return simplifyType(expandType(ty.type, ty.name, ty)); 113 | default: 114 | return ty; 115 | } 116 | } 117 | 118 | function typeEqSub(ty1: Type, ty2: Type, seen: [Type, Type][]): boolean { 119 | for (const [ty1_, ty2_] of seen) { 120 | if (typeEqNaive(ty1_, ty1, {}) && typeEqNaive(ty2_, ty2, {})) return true; 121 | } 122 | if (ty1.tag === "Rec") { 123 | return typeEqSub(simplifyType(ty1), ty2, [...seen, [ty1, ty2]]); 124 | } 125 | if (ty2.tag === "Rec") { 126 | return typeEqSub(ty1, simplifyType(ty2), [...seen, [ty1, ty2]]); 127 | } 128 | 129 | switch (ty2.tag) { 130 | case "Boolean": 131 | return ty1.tag === "Boolean"; 132 | case "Number": 133 | return ty1.tag === "Number"; 134 | case "Func": { 135 | if (ty1.tag !== "Func") return false; 136 | if (ty1.params.length !== ty2.params.length) return false; 137 | for (let i = 0; i < ty1.params.length; i++) { 138 | if (!typeEqSub(ty1.params[i].type, ty2.params[i].type, seen)) { 139 | return false; 140 | } 141 | } 142 | if (!typeEqSub(ty1.retType, ty2.retType, seen)) return false; 143 | return true; 144 | } 145 | case "Object": { 146 | if (ty1.tag !== "Object") return false; 147 | if (ty1.props.length !== ty2.props.length) return false; 148 | for (const prop2 of ty2.props) { 149 | const prop1 = ty1.props.find((prop1) => prop1.name === prop2.name); 150 | if (!prop1) return false; 151 | if (!typeEqSub(prop1.type, prop2.type, seen)) return false; 152 | } 153 | return true; 154 | } 155 | case "TypeVar": 156 | throw "unreachable"; 157 | } 158 | } 159 | 160 | function typeEq(ty1: Type, ty2: Type): boolean { 161 | return typeEqSub(ty1, ty2, []); 162 | } 163 | 164 | export function typecheck(t: Term, tyEnv: TypeEnv): Type { 165 | switch (t.tag) { 166 | case "true": 167 | return { tag: "Boolean" }; 168 | case "false": 169 | return { tag: "Boolean" }; 170 | case "if": { 171 | const condTy = simplifyType(typecheck(t.cond, tyEnv)); 172 | if (condTy.tag !== "Boolean") error("boolean expected", t.cond); 173 | const thnTy = typecheck(t.thn, tyEnv); 174 | const elsTy = typecheck(t.els, tyEnv); 175 | if (!typeEq(thnTy, elsTy)) { 176 | error("then and else have different types", t); 177 | } 178 | return thnTy; 179 | } 180 | case "number": 181 | return { tag: "Number" }; 182 | case "add": { 183 | const leftTy = simplifyType(typecheck(t.left, tyEnv)); 184 | if (leftTy.tag !== "Number") error("number expected", t.left); 185 | const rightTy = simplifyType(typecheck(t.right, tyEnv)); 186 | if (rightTy.tag !== "Number") error("number expected", t.right); 187 | return { tag: "Number" }; 188 | } 189 | case "var": { 190 | if (tyEnv[t.name] === undefined) error(`unknown variable: ${t.name}`, t); 191 | return tyEnv[t.name]; 192 | } 193 | case "func": { 194 | const newTyEnv = { ...tyEnv }; 195 | for (const { name, type } of t.params) { 196 | newTyEnv[name] = type; 197 | } 198 | const retType = typecheck(t.body, newTyEnv); 199 | return { tag: "Func", params: t.params, retType }; 200 | } 201 | case "call": { 202 | const funcTy = simplifyType(typecheck(t.func, tyEnv)); 203 | if (funcTy.tag !== "Func") error("function type expected", t.func); 204 | if (funcTy.params.length !== t.args.length) { 205 | error("wrong number of arguments", t); 206 | } 207 | for (let i = 0; i < t.args.length; i++) { 208 | const argTy = typecheck(t.args[i], tyEnv); 209 | if (!typeEq(argTy, funcTy.params[i].type)) { 210 | error("parameter type mismatch", t.args[i]); 211 | } 212 | } 213 | return funcTy.retType; 214 | } 215 | case "seq": 216 | typecheck(t.body, tyEnv); 217 | return typecheck(t.rest, tyEnv); 218 | case "const": { 219 | const ty = typecheck(t.init, tyEnv); 220 | const newTyEnv = { ...tyEnv, [t.name]: ty }; 221 | return typecheck(t.rest, newTyEnv); 222 | } 223 | case "objectNew": { 224 | const props = t.props.map( 225 | ({ name, term }) => ({ name, type: typecheck(term, tyEnv) }), 226 | ); 227 | return { tag: "Object", props }; 228 | } 229 | case "objectGet": { 230 | const objectTy = simplifyType(typecheck(t.obj, tyEnv)); 231 | if (objectTy.tag !== "Object") error("object type expected", t.obj); 232 | const prop = objectTy.props.find((prop) => prop.name === t.propName); 233 | if (!prop) error(`unknown property name: ${t.propName}`, t); 234 | return prop.type; 235 | } 236 | case "recFunc": { 237 | const funcTy: Type = { tag: "Func", params: t.params, retType: t.retType }; 238 | const newTyEnv = { ...tyEnv }; 239 | for (const { name, type } of t.params) { 240 | newTyEnv[name] = type; 241 | } 242 | newTyEnv[t.funcName] = funcTy; 243 | const retTy = typecheck(t.body, newTyEnv); 244 | if (!typeEq(t.retType, retTy)) error("wrong return type", t); 245 | const newTyEnv2 = { ...tyEnv, [t.funcName]: funcTy }; 246 | return typecheck(t.rest, newTyEnv2); 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /book/typecheckers/rec2.ts: -------------------------------------------------------------------------------- 1 | import { error } from "npm:tiny-ts-parser"; 2 | 3 | type Type = 4 | | { tag: "Boolean" } 5 | | { tag: "Number" } 6 | | { tag: "Func"; params: Param[]; retType: Type } 7 | | { tag: "Object"; props: PropertyType[] } 8 | | { tag: "TaggedUnion"; variants: VariantType[] } 9 | | { tag: "Rec"; name: string; type: Type } 10 | | { tag: "TypeVar"; name: string }; 11 | 12 | type Param = { name: string; type: Type }; 13 | type PropertyType = { name: string; type: Type }; 14 | type VariantType = { tagLabel: string; props: PropertyType[] }; 15 | 16 | type Term = 17 | | { tag: "true" } 18 | | { tag: "false" } 19 | | { tag: "if"; cond: Term; thn: Term; els: Term } 20 | | { tag: "number"; n: number } 21 | | { tag: "add"; left: Term; right: Term } 22 | | { tag: "var"; name: string } 23 | | { tag: "func"; params: Param[]; body: Term } 24 | | { tag: "call"; func: Term; args: Term[] } 25 | | { tag: "seq"; body: Term; rest: Term } 26 | | { tag: "const"; name: string; init: Term; rest: Term } 27 | | { tag: "objectNew"; props: PropertyTerm[] } 28 | | { tag: "objectGet"; obj: Term; propName: string } 29 | | { tag: "taggedUnionNew"; tagLabel: string; props: PropertyTerm[]; as: Type } 30 | | { tag: "taggedUnionGet"; varName: string; clauses: VariantTerm[] } 31 | | { 32 | tag: "recFunc"; 33 | funcName: string; 34 | params: Param[]; 35 | retType: Type; 36 | body: Term; 37 | rest: Term; 38 | }; 39 | 40 | type PropertyTerm = { name: string; term: Term }; 41 | type VariantTerm = { tagLabel: string; term: Term }; 42 | 43 | type TypeEnv = Record; 44 | 45 | function typeEqNaive(ty1: Type, ty2: Type, map: Record): boolean { 46 | switch (ty2.tag) { 47 | case "Boolean": 48 | case "Number": 49 | return ty1.tag === ty2.tag; 50 | case "Func": { 51 | if (ty1.tag !== "Func") return false; 52 | for (let i = 0; i < ty1.params.length; i++) { 53 | if (!typeEqNaive(ty1.params[i].type, ty2.params[i].type, map)) { 54 | return false; 55 | } 56 | } 57 | if (!typeEqNaive(ty1.retType, ty2.retType, map)) return false; 58 | return true; 59 | } 60 | case "Object": { 61 | if (ty1.tag !== "Object") return false; 62 | if (ty1.props.length !== ty2.props.length) return false; 63 | for (const prop1 of ty1.props) { 64 | const prop2 = ty2.props.find((prop2) => prop1.name === prop2.name); 65 | if (!prop2) return false; 66 | if (!typeEqNaive(prop1.type, prop2.type, map)) return false; 67 | } 68 | return true; 69 | } 70 | case "TaggedUnion": { 71 | if (ty1.tag !== "TaggedUnion") return false; 72 | if (ty1.variants.length !== ty2.variants.length) return false; 73 | for (const variant1 of ty1.variants) { 74 | const variant2 = ty2.variants.find( 75 | (variant2) => variant1.tagLabel === variant2.tagLabel, 76 | ); 77 | if (!variant2) return false; 78 | if (variant1.props.length !== variant2.props.length) return false; 79 | for (const prop1 of variant1.props) { 80 | const prop2 = variant2.props.find((prop2) => prop1.name === prop2.name); 81 | if (!prop2) return false; 82 | if (!typeEqNaive(prop1.type, prop2.type, map)) return false; 83 | } 84 | } 85 | return true; 86 | } 87 | case "Rec": { 88 | if (ty1.tag !== "Rec") return false; 89 | const newMap = { ...map, [ty1.name]: ty2.name }; 90 | return typeEqNaive(ty1.type, ty2.type, newMap); 91 | } 92 | case "TypeVar": { 93 | if (ty1.tag !== "TypeVar") return false; 94 | if (map[ty1.name] === undefined) { 95 | throw new Error(`unknown type variable: ${ty1.name}`); 96 | } 97 | return map[ty1.name] === ty2.name; 98 | } 99 | } 100 | } 101 | 102 | function expandType(ty: Type, tyVarName: string, repTy: Type): Type { 103 | switch (ty.tag) { 104 | case "Boolean": 105 | case "Number": 106 | return ty; 107 | case "Func": { 108 | const params = ty.params.map( 109 | ({ name, type }) => ({ name, type: expandType(type, tyVarName, repTy) }), 110 | ); 111 | const retType = expandType(ty.retType, tyVarName, repTy); 112 | return { tag: "Func", params, retType }; 113 | } 114 | case "Object": { 115 | const props = ty.props.map( 116 | ({ name, type }) => ({ name, type: expandType(type, tyVarName, repTy) }), 117 | ); 118 | return { tag: "Object", props }; 119 | } 120 | case "TaggedUnion": { 121 | const variants = ty.variants.map(({ tagLabel, props }) => { 122 | const newProps = props.map( 123 | ({ name, type }) => ({ name, type: expandType(type, tyVarName, repTy) }), 124 | ); 125 | return { tagLabel, props: newProps }; 126 | }); 127 | return { tag: "TaggedUnion", variants }; 128 | } 129 | case "Rec": { 130 | if (ty.name === tyVarName) return ty; 131 | const newType = expandType(ty.type, tyVarName, repTy); 132 | return { tag: "Rec", name: ty.name, type: newType }; 133 | } 134 | case "TypeVar": { 135 | return ty.name === tyVarName ? repTy : ty; 136 | } 137 | } 138 | } 139 | 140 | function simplifyType(ty: Type): Type { 141 | switch (ty.tag) { 142 | case "Rec": 143 | return simplifyType(expandType(ty.type, ty.name, ty)); 144 | default: 145 | return ty; 146 | } 147 | } 148 | 149 | function typeEqSub(ty1: Type, ty2: Type, seen: [Type, Type][]): boolean { 150 | for (const [ty1_, ty2_] of seen) { 151 | if (typeEqNaive(ty1_, ty1, {}) && typeEqNaive(ty2_, ty2, {})) return true; 152 | } 153 | if (ty1.tag === "Rec") { 154 | return typeEqSub(simplifyType(ty1), ty2, [...seen, [ty1, ty2]]); 155 | } 156 | if (ty2.tag === "Rec") { 157 | return typeEqSub(ty1, simplifyType(ty2), [...seen, [ty1, ty2]]); 158 | } 159 | 160 | switch (ty2.tag) { 161 | case "Boolean": 162 | return ty1.tag === "Boolean"; 163 | case "Number": 164 | return ty1.tag === "Number"; 165 | case "Func": { 166 | if (ty1.tag !== "Func") return false; 167 | if (ty1.params.length !== ty2.params.length) return false; 168 | for (let i = 0; i < ty1.params.length; i++) { 169 | if (!typeEqSub(ty1.params[i].type, ty2.params[i].type, seen)) { 170 | return false; 171 | } 172 | } 173 | if (!typeEqSub(ty1.retType, ty2.retType, seen)) return false; 174 | return true; 175 | } 176 | case "Object": { 177 | if (ty1.tag !== "Object") return false; 178 | if (ty1.props.length !== ty2.props.length) return false; 179 | for (const prop2 of ty2.props) { 180 | const prop1 = ty1.props.find((prop1) => prop1.name === prop2.name); 181 | if (!prop1) return false; 182 | if (!typeEqSub(prop1.type, prop2.type, seen)) return false; 183 | } 184 | return true; 185 | } 186 | case "TaggedUnion": { 187 | if (ty1.tag !== "TaggedUnion") return false; 188 | if (ty1.variants.length !== ty2.variants.length) return false; 189 | for (const variant1 of ty1.variants) { 190 | const variant2 = ty2.variants.find( 191 | (variant2) => variant1.tagLabel === variant2.tagLabel, 192 | ); 193 | if (!variant2) return false; 194 | if (variant1.props.length !== variant2.props.length) return false; 195 | for (const prop1 of variant1.props) { 196 | const prop2 = variant2.props.find((prop2) => prop1.name === prop2.name); 197 | if (!prop2) return false; 198 | if (!typeEqSub(prop1.type, prop2.type, seen)) return false; 199 | } 200 | } 201 | return true; 202 | } 203 | case "TypeVar": 204 | throw "unreachable"; 205 | } 206 | } 207 | 208 | function typeEq(ty1: Type, ty2: Type): boolean { 209 | return typeEqSub(ty1, ty2, []); 210 | } 211 | 212 | export function typecheck(t: Term, tyEnv: TypeEnv): Type { 213 | switch (t.tag) { 214 | case "true": 215 | return { tag: "Boolean" }; 216 | case "false": 217 | return { tag: "Boolean" }; 218 | case "if": { 219 | const condTy = simplifyType(typecheck(t.cond, tyEnv)); 220 | if (condTy.tag !== "Boolean") error("boolean expected", t.cond); 221 | const thnTy = typecheck(t.thn, tyEnv); 222 | const elsTy = typecheck(t.els, tyEnv); 223 | if (!typeEq(thnTy, elsTy)) { 224 | error("then and else have different types", t); 225 | } 226 | return thnTy; 227 | } 228 | case "number": 229 | return { tag: "Number" }; 230 | case "add": { 231 | const leftTy = simplifyType(typecheck(t.left, tyEnv)); 232 | if (leftTy.tag !== "Number") error("number expected", t.left); 233 | const rightTy = simplifyType(typecheck(t.right, tyEnv)); 234 | if (rightTy.tag !== "Number") error("number expected", t.right); 235 | return { tag: "Number" }; 236 | } 237 | case "var": { 238 | if (tyEnv[t.name] === undefined) error(`unknown variable: ${t.name}`, t); 239 | return tyEnv[t.name]; 240 | } 241 | case "func": { 242 | const newTyEnv = { ...tyEnv }; 243 | for (const { name, type } of t.params) { 244 | newTyEnv[name] = type; 245 | } 246 | const retType = typecheck(t.body, newTyEnv); 247 | return { tag: "Func", params: t.params, retType }; 248 | } 249 | case "call": { 250 | const funcTy = simplifyType(typecheck(t.func, tyEnv)); 251 | if (funcTy.tag !== "Func") error("function type expected", t.func); 252 | if (funcTy.params.length !== t.args.length) { 253 | error("wrong number of arguments", t); 254 | } 255 | for (let i = 0; i < t.args.length; i++) { 256 | const argTy = typecheck(t.args[i], tyEnv); 257 | if (!typeEq(argTy, funcTy.params[i].type)) { 258 | error("parameter type mismatch", t.args[i]); 259 | } 260 | } 261 | return funcTy.retType; 262 | } 263 | case "seq": 264 | typecheck(t.body, tyEnv); 265 | return typecheck(t.rest, tyEnv); 266 | case "const": { 267 | const ty = typecheck(t.init, tyEnv); 268 | const newTyEnv = { ...tyEnv, [t.name]: ty }; 269 | return typecheck(t.rest, newTyEnv); 270 | } 271 | case "objectNew": { 272 | const props = t.props.map( 273 | ({ name, term }) => ({ name, type: typecheck(term, tyEnv) }), 274 | ); 275 | return { tag: "Object", props }; 276 | } 277 | case "objectGet": { 278 | const objectTy = simplifyType(typecheck(t.obj, tyEnv)); 279 | if (objectTy.tag !== "Object") error("object type expected", t.obj); 280 | const prop = objectTy.props.find((prop) => prop.name === t.propName); 281 | if (!prop) error(`unknown property name: ${t.propName}`, t); 282 | return prop.type; 283 | } 284 | case "taggedUnionNew": { 285 | const asTy = simplifyType(t.as); 286 | if (asTy.tag !== "TaggedUnion") { 287 | error(`"as" must have a tagged union type`, t); 288 | } 289 | const variant = asTy.variants.find( 290 | (variant) => variant.tagLabel === t.tagLabel, 291 | ); 292 | if (!variant) error(`unknown variant tag: ${t.tagLabel}`, t); 293 | for (const prop1 of t.props) { 294 | const prop2 = variant.props.find((prop2) => prop1.name === prop2.name); 295 | if (!prop2) error(`unknown property: ${prop1.name}`, t); 296 | const actualTy = typecheck(prop1.term, tyEnv); 297 | if (!typeEq(actualTy, prop2.type)) { 298 | error("tagged union's term has a wrong type", prop1.term); 299 | } 300 | } 301 | return t.as; 302 | } 303 | case "taggedUnionGet": { 304 | const variantTy = simplifyType(tyEnv[t.varName]); 305 | if (variantTy.tag !== "TaggedUnion") { 306 | error(`variable ${t.varName} must have a tagged union type`, t); 307 | } 308 | let retTy: Type | null = null; 309 | for (const clause of t.clauses) { 310 | const variant = variantTy.variants.find( 311 | (variant) => variant.tagLabel === clause.tagLabel, 312 | ); 313 | if (!variant) { 314 | error(`tagged union type has no case: ${clause.tagLabel}`, clause.term); 315 | } 316 | const localTy: Type = { tag: "Object", props: variant.props }; 317 | const newTyEnv = { ...tyEnv, [t.varName]: localTy }; 318 | const clauseTy = typecheck(clause.term, newTyEnv); 319 | if (retTy) { 320 | if (!typeEq(retTy, clauseTy)) { 321 | error("clauses has different types", clause.term); 322 | } 323 | } else { 324 | retTy = clauseTy; 325 | } 326 | } 327 | if (variantTy.variants.length !== t.clauses.length) { 328 | error("switch case is not exhaustive", t); 329 | } 330 | return retTy!; 331 | } 332 | case "recFunc": { 333 | const funcTy: Type = { tag: "Func", params: t.params, retType: t.retType }; 334 | const newTyEnv = { ...tyEnv }; 335 | for (const { name, type } of t.params) { 336 | newTyEnv[name] = type; 337 | } 338 | newTyEnv[t.funcName] = funcTy; 339 | const retTy = typecheck(t.body, newTyEnv); 340 | if (!typeEq(t.retType, retTy)) error("wrong return type", t); 341 | const newTyEnv2 = { ...tyEnv, [t.funcName]: funcTy }; 342 | return typecheck(t.rest, newTyEnv2); 343 | } 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /book/typecheckers/rec2_test.ts: -------------------------------------------------------------------------------- 1 | import test from "node:test"; 2 | import assert from "node:assert"; 3 | import { parseRec2, typeShow } from "npm:tiny-ts-parser"; 4 | import { typecheck } from "./rec2.ts"; 5 | 6 | function run(code: string) { 7 | return typecheck(parseRec2(code), {}); 8 | } 9 | function ok(expected: string, code: string) { 10 | assert.equal(expected, typeShow(run(code))); 11 | } 12 | function ng(expected: RegExp, code: string) { 13 | assert.throws(() => { 14 | run(code); 15 | return true; 16 | }, expected); 17 | } 18 | 19 | test("fib", () => 20 | ok( 21 | "(mu NumStream. { num: number; rest: () => NumStream })", 22 | ` 23 | type NumStream = { num: number; rest: () => NumStream }; 24 | 25 | function numbers(n: number): NumStream { 26 | return { num: n, rest: () => numbers(n + 1) }; 27 | } 28 | 29 | const ns1 = numbers(1); 30 | const ns2 = (ns1.rest)(); 31 | const ns3 = (ns2.rest)(); 32 | ns3 33 | `, 34 | )); 35 | 36 | test("mutual recursion 1", () => 37 | ok( 38 | "number", 39 | ` 40 | type A = { aa: B }; 41 | type B = { bb: A }; 42 | function f(x: A): boolean { 43 | return f({ aa: { bb: x } }); 44 | } 45 | 1; 46 | `, 47 | )); 48 | 49 | test("mutual recursion 2", () => 50 | ng( 51 | /test.ts:5:14-5:39 parameter type mismatch/, 52 | ` 53 | type A = { aa: B }; 54 | type B = { bb: A }; 55 | function f(x: A): boolean { 56 | return f({ aa: { bb: { aa: x } } }); 57 | } 58 | 1; 59 | `, 60 | )); 61 | 62 | test("mutual recursion 3", () => 63 | ok( 64 | "number", 65 | ` 66 | type A = { tag: "ab", val: B } | { tag: "ac", val: C }; 67 | type B = { tag: "bc", val: C } | { tag: "ba", val: A }; 68 | type C = { tag: "ca", val: A } | { tag: "cb", val: B }; 69 | function f(a: A): boolean { 70 | return true; 71 | } 72 | function g(b: B, c: C): boolean { 73 | const newA = { tag: "ab", val: b }; 74 | return f(newA); 75 | } 76 | 1; 77 | `, 78 | )); 79 | 80 | test("numlist", () => 81 | ok( 82 | "{ " + 83 | `isnil: (l: (mu NumList. ({ tag: "cons"; num: number; tail: NumList } | { tag: "nil" }))) => boolean; ` + 84 | `hd: (l: (mu NumList. ({ tag: "cons"; num: number; tail: NumList } | { tag: "nil" }))) => number; ` + 85 | `tl: (l: (mu NumList. ({ tag: "cons"; num: number; tail: NumList } | { tag: "nil" }))) => ` + 86 | `(mu NumList. ({ tag: "cons"; num: number; tail: NumList } | { tag: "nil" })); ` + 87 | `sum: (l: (mu NumList. ({ tag: "cons"; num: number; tail: NumList } | { tag: "nil" }))) => number; ` + 88 | `hd_tl_result: number; ` + 89 | `sum_result: number ` + 90 | "}", 91 | ` 92 | type NumList = 93 | | { tag: "cons"; num: number; tail: NumList } 94 | | { tag: "nil" } 95 | ; 96 | const cons = (num: number, tail: NumList) => { 97 | return { tag: "cons", num: num, tail: tail }; 98 | }; 99 | const nil = { tag: "nil" }; 100 | const isnil = (l: NumList) => { 101 | switch (l.tag) { 102 | case "cons": 103 | return false; 104 | case "nil": 105 | return true; 106 | } 107 | }; 108 | const hd = (l: NumList) => { 109 | switch (l.tag) { 110 | case "cons": 111 | return l.num; 112 | case "nil": 113 | return 0; /* dummy */ 114 | } 115 | }; 116 | const tl = (l: NumList) => { 117 | switch (l.tag) { 118 | case "cons": 119 | return l.tail; 120 | case "nil": 121 | return nil; 122 | } 123 | }; 124 | function sum(l: NumList): number { 125 | if (isnil(l)) 126 | return 0; 127 | else 128 | return hd(l) + sum(tl(l)); 129 | } 130 | const l = cons(1, cons(2, cons(3, nil))); 131 | ({ 132 | isnil, 133 | hd, 134 | tl, 135 | sum, 136 | hd_tl_result: hd(tl(tl(tl(l)))), 137 | sum_result: sum(l), 138 | }); 139 | `, 140 | )); 141 | -------------------------------------------------------------------------------- /book/typecheckers/rec_test.ts: -------------------------------------------------------------------------------- 1 | import test from "node:test"; 2 | import assert from "node:assert"; 3 | import { parseRec, typeShow } from "npm:tiny-ts-parser"; 4 | import { typecheck } from "./rec.ts"; 5 | 6 | function run(code: string) { 7 | return typecheck(parseRec(code), {}); 8 | } 9 | function ok(expected: string, code: string) { 10 | assert.equal(expected, typeShow(run(code))); 11 | } 12 | function ng(expected: RegExp, code: string) { 13 | assert.throws(() => { 14 | run(code); 15 | return true; 16 | }, expected); 17 | } 18 | 19 | test("rec hungry 1", () => 20 | ok( 21 | "(f: (mu Hungry. (x: number) => Hungry)) => (mu Hungry. (x: number) => Hungry)", 22 | ` 23 | type Hungry = (x: number) => Hungry; 24 | (f: Hungry) => { 25 | return f(0)(1)(2)(3)(4)(5); 26 | } 27 | `, 28 | )); 29 | 30 | test("rec hungry 2", () => 31 | ok( 32 | "(mu Hungry. (x: number) => Hungry)", 33 | ` 34 | type Hungry = (x: number) => Hungry; 35 | function hungry(x: number): Hungry { 36 | return hungry; 37 | } 38 | hungry(0)(1)(2)(3)(4)(5); 39 | `, 40 | )); 41 | 42 | test("fib", () => 43 | ok( 44 | "(mu NumStream. { num: number; rest: () => NumStream })", 45 | ` 46 | type NumStream = { num: number; rest: () => NumStream }; 47 | 48 | function numbers(n: number): NumStream { 49 | return { num: n, rest: () => numbers(n + 1) }; 50 | } 51 | 52 | const ns1 = numbers(1); 53 | const ns2 = (ns1.rest)(); 54 | const ns3 = (ns2.rest)(); 55 | ns3 56 | `, 57 | )); 58 | 59 | test("mutual recursion 1", () => 60 | ok( 61 | "number", 62 | ` 63 | type A = { aa: B }; 64 | type B = { bb: A }; 65 | function f(x: A): boolean { 66 | return f({ aa: { bb: x } }); 67 | } 68 | 1; 69 | `, 70 | )); 71 | 72 | test("mutual recursion 2", () => 73 | ng( 74 | /test.ts:5:14-5:39 parameter type mismatch/, 75 | ` 76 | type A = { aa: B }; 77 | type B = { bb: A }; 78 | function f(x: A): boolean { 79 | return f({ aa: { bb: { aa: x } } }); 80 | } 81 | 1; 82 | `, 83 | )); 84 | 85 | test("mutual recursion 3", () => 86 | ok( 87 | "(b: (mu B. { b: { a: B } })) => boolean", 88 | ` 89 | type A = { a: { b: A } }; 90 | type B = { b: { a: B } }; 91 | const f = (x: A): boolean => true; 92 | (b: B) => f({ a: b }); 93 | `, 94 | )); 95 | -------------------------------------------------------------------------------- /book/typecheckers/recfunc.ts: -------------------------------------------------------------------------------- 1 | import { error } from "npm:tiny-ts-parser"; 2 | 3 | type Type = 4 | | { tag: "Boolean" } 5 | | { tag: "Number" } 6 | | { tag: "Func"; params: Param[]; retType: Type }; 7 | 8 | type Param = { name: string; type: Type }; 9 | 10 | type Term = 11 | | { tag: "true" } 12 | | { tag: "false" } 13 | | { tag: "if"; cond: Term; thn: Term; els: Term } 14 | | { tag: "number"; n: number } 15 | | { tag: "add"; left: Term; right: Term } 16 | | { tag: "var"; name: string } 17 | | { tag: "func"; params: Param[]; body: Term } 18 | | { tag: "call"; func: Term; args: Term[] } 19 | | { tag: "seq"; body: Term; rest: Term } 20 | | { tag: "const"; name: string; init: Term; rest: Term } 21 | | { 22 | tag: "recFunc"; 23 | funcName: string; 24 | params: Param[]; 25 | retType: Type; 26 | body: Term; 27 | rest: Term; 28 | }; 29 | 30 | type TypeEnv = Record; 31 | 32 | function typeEq(ty1: Type, ty2: Type): boolean { 33 | switch (ty2.tag) { 34 | case "Boolean": 35 | return ty1.tag === "Boolean"; 36 | case "Number": 37 | return ty1.tag === "Number"; 38 | case "Func": { 39 | if (ty1.tag !== "Func") return false; 40 | if (ty1.params.length !== ty2.params.length) return false; 41 | for (let i = 0; i < ty1.params.length; i++) { 42 | if (!typeEq(ty1.params[i].type, ty2.params[i].type)) { 43 | return false; 44 | } 45 | } 46 | if (!typeEq(ty1.retType, ty2.retType)) return false; 47 | return true; 48 | } 49 | } 50 | } 51 | 52 | export function typecheck(t: Term, tyEnv: TypeEnv): Type { 53 | switch (t.tag) { 54 | case "true": 55 | return { tag: "Boolean" }; 56 | case "false": 57 | return { tag: "Boolean" }; 58 | case "if": { 59 | const condTy = typecheck(t.cond, tyEnv); 60 | if (condTy.tag !== "Boolean") error("boolean expected", t.cond); 61 | const thnTy = typecheck(t.thn, tyEnv); 62 | const elsTy = typecheck(t.els, tyEnv); 63 | if (!typeEq(thnTy, elsTy)) { 64 | error("then and else have different types", t); 65 | } 66 | return thnTy; 67 | } 68 | case "number": 69 | return { tag: "Number" }; 70 | case "add": { 71 | const leftTy = typecheck(t.left, tyEnv); 72 | if (leftTy.tag !== "Number") error("number expected", t.left); 73 | const rightTy = typecheck(t.right, tyEnv); 74 | if (rightTy.tag !== "Number") error("number expected", t.right); 75 | return { tag: "Number" }; 76 | } 77 | case "var": { 78 | if (tyEnv[t.name] === undefined) error(`unknown variable: ${t.name}`, t); 79 | return tyEnv[t.name]; 80 | } 81 | case "func": { 82 | const newTyEnv = { ...tyEnv }; 83 | for (const { name, type } of t.params) { 84 | newTyEnv[name] = type; 85 | } 86 | const retType = typecheck(t.body, newTyEnv); 87 | return { tag: "Func", params: t.params, retType }; 88 | } 89 | case "call": { 90 | const funcTy = typecheck(t.func, tyEnv); 91 | if (funcTy.tag !== "Func") error("function type expected", t.func); 92 | if (funcTy.params.length !== t.args.length) { 93 | error("wrong number of arguments", t); 94 | } 95 | for (let i = 0; i < t.args.length; i++) { 96 | const argTy = typecheck(t.args[i], tyEnv); 97 | if (!typeEq(argTy, funcTy.params[i].type)) { 98 | error("parameter type mismatch", t.args[i]); 99 | } 100 | } 101 | return funcTy.retType; 102 | } 103 | case "seq": 104 | typecheck(t.body, tyEnv); 105 | return typecheck(t.rest, tyEnv); 106 | case "const": { 107 | const ty = typecheck(t.init, tyEnv); 108 | const newTyEnv = { ...tyEnv, [t.name]: ty }; 109 | return typecheck(t.rest, newTyEnv); 110 | } 111 | case "recFunc": { 112 | const funcTy: Type = { tag: "Func", params: t.params, retType: t.retType }; 113 | const newTyEnv = { ...tyEnv }; 114 | for (const { name, type } of t.params) { 115 | newTyEnv[name] = type; 116 | } 117 | newTyEnv[t.funcName] = funcTy; 118 | const retTy = typecheck(t.body, newTyEnv); 119 | if (!typeEq(t.retType, retTy)) error("wrong return type", t); 120 | const newTyEnv2 = { ...tyEnv, [t.funcName]: funcTy }; 121 | return typecheck(t.rest, newTyEnv2); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /book/typecheckers/recfunc2.ts: -------------------------------------------------------------------------------- 1 | import { error } from "npm:tiny-ts-parser"; 2 | 3 | type Type = 4 | | { tag: "Boolean" } 5 | | { tag: "Number" } 6 | | { tag: "Func"; params: Param[]; retType: Type }; 7 | 8 | type Param = { name: string; type: Type }; 9 | 10 | type Term = 11 | | { tag: "true" } 12 | | { tag: "false" } 13 | | { tag: "if"; cond: Term; thn: Term; els: Term } 14 | | { tag: "number"; n: number } 15 | | { tag: "add"; left: Term; right: Term } 16 | | { tag: "var"; name: string } 17 | | { tag: "func"; params: Param[]; retType?: Type; body: Term } 18 | | { tag: "call"; func: Term; args: Term[] } 19 | | { tag: "seq"; body: Term; rest: Term } 20 | | { tag: "const"; name: string; init: Term; rest: Term } 21 | | { 22 | tag: "recFunc"; 23 | funcName: string; 24 | params: Param[]; 25 | retType: Type; 26 | body: Term; 27 | rest: Term; 28 | }; 29 | 30 | type TypeEnv = Record; 31 | 32 | function typeEq(ty1: Type, ty2: Type): boolean { 33 | switch (ty2.tag) { 34 | case "Boolean": 35 | return ty1.tag === "Boolean"; 36 | case "Number": 37 | return ty1.tag === "Number"; 38 | case "Func": { 39 | if (ty1.tag !== "Func") return false; 40 | if (ty1.params.length !== ty2.params.length) return false; 41 | for (let i = 0; i < ty1.params.length; i++) { 42 | if (!typeEq(ty1.params[i].type, ty2.params[i].type)) { 43 | return false; 44 | } 45 | } 46 | if (!typeEq(ty1.retType, ty2.retType)) return false; 47 | return true; 48 | } 49 | } 50 | } 51 | 52 | export function typecheck(t: Term, tyEnv: TypeEnv): Type { 53 | switch (t.tag) { 54 | case "true": 55 | return { tag: "Boolean" }; 56 | case "false": 57 | return { tag: "Boolean" }; 58 | case "if": { 59 | const condTy = typecheck(t.cond, tyEnv); 60 | if (condTy.tag !== "Boolean") error("boolean expected", t.cond); 61 | const thnTy = typecheck(t.thn, tyEnv); 62 | const elsTy = typecheck(t.els, tyEnv); 63 | if (!typeEq(thnTy, elsTy)) { 64 | error("then and else have different types", t); 65 | } 66 | return thnTy; 67 | } 68 | case "number": 69 | return { tag: "Number" }; 70 | case "add": { 71 | const leftTy = typecheck(t.left, tyEnv); 72 | if (leftTy.tag !== "Number") error("number expected", t.left); 73 | const rightTy = typecheck(t.right, tyEnv); 74 | if (rightTy.tag !== "Number") error("number expected", t.right); 75 | return { tag: "Number" }; 76 | } 77 | case "var": { 78 | if (tyEnv[t.name] === undefined) error(`unknown variable: ${t.name}`, t); 79 | return tyEnv[t.name]; 80 | } 81 | case "func": { 82 | const newTyEnv = { ...tyEnv }; 83 | for (const { name, type } of t.params) { 84 | newTyEnv[name] = type; 85 | } 86 | const retType = typecheck(t.body, newTyEnv); 87 | if (t.retType) { 88 | if (!typeEq(retType, t.retType)) error("wrong return type", t.body); 89 | } 90 | return { tag: "Func", params: t.params, retType }; 91 | } 92 | case "call": { 93 | const funcTy = typecheck(t.func, tyEnv); 94 | if (funcTy.tag !== "Func") error("function type expected", t.func); 95 | if (funcTy.params.length !== t.args.length) { 96 | error("wrong number of arguments", t); 97 | } 98 | for (let i = 0; i < t.args.length; i++) { 99 | const argTy = typecheck(t.args[i], tyEnv); 100 | if (!typeEq(argTy, funcTy.params[i].type)) { 101 | error("parameter type mismatch", t.args[i]); 102 | } 103 | } 104 | return funcTy.retType; 105 | } 106 | case "seq": 107 | typecheck(t.body, tyEnv); 108 | return typecheck(t.rest, tyEnv); 109 | case "const": { 110 | if (t.init.tag === "func") { 111 | if (!t.init.retType) error("return type must be specified", t.init); 112 | const funcTy: Type = { 113 | tag: "Func", 114 | params: t.init.params, 115 | retType: t.init.retType, 116 | }; 117 | const newTyEnv = { ...tyEnv }; 118 | for (const { name, type } of t.init.params) { 119 | newTyEnv[name] = type; 120 | } 121 | const newTyEnv2 = { ...newTyEnv, [t.name]: funcTy }; 122 | if (!typeEq(t.init.retType, typecheck(t.init.body, newTyEnv2))) { 123 | error("wrong return type", t.init.body); 124 | } 125 | const newTyEnv3 = { ...tyEnv, [t.name]: funcTy }; 126 | return typecheck(t.rest, newTyEnv3); 127 | } else { 128 | const ty = typecheck(t.init, tyEnv); 129 | const newTyEnv = { ...tyEnv, [t.name]: ty }; 130 | return typecheck(t.rest, newTyEnv); 131 | } 132 | } 133 | case "recFunc": { 134 | const funcTy: Type = { tag: "Func", params: t.params, retType: t.retType }; 135 | const newTyEnv = { ...tyEnv }; 136 | for (const { name, type } of t.params) { 137 | newTyEnv[name] = type; 138 | } 139 | newTyEnv[t.funcName] = funcTy; 140 | const retTy = typecheck(t.body, newTyEnv); 141 | if (!typeEq(t.retType, retTy)) error("wrong return type", t); 142 | const newTyEnv2 = { ...tyEnv, [t.funcName]: funcTy }; 143 | return typecheck(t.rest, newTyEnv2); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /book/typecheckers/recfunc2_test.ts: -------------------------------------------------------------------------------- 1 | import test from "node:test"; 2 | import assert from "node:assert"; 3 | import { parseRecFunc, typeShow } from "npm:tiny-ts-parser"; 4 | import { typecheck } from "./recfunc2.ts"; 5 | 6 | function run(code: string) { 7 | return typecheck(parseRecFunc(code), {}); 8 | } 9 | function ok(expected: string, code: string) { 10 | assert.equal(expected, typeShow(run(code))); 11 | } 12 | function ng(expected: RegExp, code: string) { 13 | assert.throws(() => { 14 | run(code); 15 | return true; 16 | }, expected); 17 | } 18 | 19 | test("recursive func", () => 20 | ng( 21 | /test.ts:1:25-1:26 wrong return type/, 22 | `(x: number): boolean => x`, 23 | )); 24 | test("recursive func", () => 25 | ok( 26 | "(n: number) => number", 27 | ` 28 | const fib = (n: number): number => { 29 | return fib(n + 1) + fib(n); 30 | } 31 | fib; 32 | `, 33 | )); 34 | -------------------------------------------------------------------------------- /book/typecheckers/recfunc_test.ts: -------------------------------------------------------------------------------- 1 | import test from "node:test"; 2 | import assert from "node:assert"; 3 | import { parseRecFunc, typeShow } from "npm:tiny-ts-parser"; 4 | import { typecheck } from "./recfunc.ts"; 5 | 6 | function run(code: string) { 7 | return typecheck(parseRecFunc(code), {}); 8 | } 9 | function ok(expected: string, code: string) { 10 | assert.equal(expected, typeShow(run(code))); 11 | } 12 | function ng(expected: RegExp, code: string) { 13 | assert.throws(() => { 14 | run(code); 15 | return true; 16 | }, expected); 17 | } 18 | 19 | test("recursive func", () => 20 | ok( 21 | "(n: number) => number", 22 | ` 23 | function fib(n: number): number { 24 | return fib(n + 1) + fib(n); 25 | } 26 | fib; 27 | `, 28 | )); 29 | test("recursive func (empty string)", () => 30 | ok( 31 | "(n: number) => number", 32 | ` 33 | function fib(n: number): number { 34 | return fib(n + 1) + fib(n); 35 | }; 36 | fib; 37 | `, 38 | )); 39 | test("recursive func error", () => 40 | ng( 41 | /wrong return type/, 42 | ` 43 | function f(): number { 44 | return true; 45 | } 46 | f; 47 | `, 48 | )); 49 | -------------------------------------------------------------------------------- /book/typecheckers/sub.ts: -------------------------------------------------------------------------------- 1 | import { error } from "npm:tiny-ts-parser"; 2 | 3 | type Type = 4 | | { tag: "Boolean" } 5 | | { tag: "Number" } 6 | | { tag: "Func"; params: Param[]; retType: Type } 7 | | { tag: "Object"; props: PropertyType[] }; 8 | 9 | type Param = { name: string; type: Type }; 10 | type PropertyType = { name: string; type: Type }; 11 | 12 | type Term = 13 | | { tag: "true" } 14 | | { tag: "false" } 15 | //| { tag: "if"; cond: Term; thn: Term; els: Term } 16 | | { tag: "number"; n: number } 17 | | { tag: "add"; left: Term; right: Term } 18 | | { tag: "var"; name: string } 19 | | { tag: "func"; params: Param[]; body: Term } 20 | | { tag: "call"; func: Term; args: Term[] } 21 | | { tag: "seq"; body: Term; rest: Term } 22 | | { tag: "const"; name: string; init: Term; rest: Term } 23 | | { tag: "objectNew"; props: PropertyTerm[] } 24 | | { tag: "objectGet"; obj: Term; propName: string }; 25 | 26 | type PropertyTerm = { name: string; term: Term }; 27 | 28 | type TypeEnv = Record; 29 | 30 | function subtype(ty1: Type, ty2: Type): boolean { 31 | switch (ty2.tag) { 32 | case "Boolean": 33 | return ty1.tag === "Boolean"; 34 | case "Number": 35 | return ty1.tag === "Number"; 36 | case "Func": { 37 | if (ty1.tag !== "Func") return false; 38 | if (ty1.params.length !== ty2.params.length) return false; 39 | for (let i = 0; i < ty1.params.length; i++) { 40 | if (!subtype(ty2.params[i].type, ty1.params[i].type)) { 41 | return false; // contravariant 42 | } 43 | } 44 | if (!subtype(ty1.retType, ty2.retType)) return false; 45 | return true; 46 | } 47 | case "Object": { 48 | if (ty1.tag !== "Object") return false; 49 | //if (ty1.props.length !== ty2.props.length) return false; 50 | for (const prop2 of ty2.props) { 51 | const prop1 = ty1.props.find((prop1) => prop1.name === prop2.name); 52 | if (!prop1) return false; 53 | if (!subtype(prop1.type, prop2.type)) return false; 54 | } 55 | return true; 56 | } 57 | } 58 | } 59 | 60 | export function typecheck(t: Term, tyEnv: TypeEnv): Type { 61 | switch (t.tag) { 62 | case "true": 63 | return { tag: "Boolean" }; 64 | case "false": 65 | return { tag: "Boolean" }; 66 | //case "if": { 67 | // const condTy = typecheck(t.cond, tyEnv); 68 | // if (condTy.tag !== "Boolean") error("boolean expected", t.cond); 69 | // const thnTy = typecheck(t.thn, tyEnv); 70 | // const elsTy = typecheck(t.els, tyEnv); 71 | // if (!subtype(thnTy, elsTy)) { 72 | // error("then and else have different types", t); 73 | // } 74 | // return thnTy; 75 | //} 76 | case "number": 77 | return { tag: "Number" }; 78 | case "add": { 79 | const leftTy = typecheck(t.left, tyEnv); 80 | if (leftTy.tag !== "Number") error("number expected", t.left); 81 | const rightTy = typecheck(t.right, tyEnv); 82 | if (rightTy.tag !== "Number") error("number expected", t.right); 83 | return { tag: "Number" }; 84 | } 85 | case "var": { 86 | if (tyEnv[t.name] === undefined) error(`unknown variable: ${t.name}`, t); 87 | return tyEnv[t.name]; 88 | } 89 | case "func": { 90 | const newTyEnv = { ...tyEnv }; 91 | for (const { name, type } of t.params) { 92 | newTyEnv[name] = type; 93 | } 94 | const retType = typecheck(t.body, newTyEnv); 95 | return { tag: "Func", params: t.params, retType }; 96 | } 97 | case "call": { 98 | const funcTy = typecheck(t.func, tyEnv); 99 | if (funcTy.tag !== "Func") error("function type expected", t.func); 100 | if (funcTy.params.length !== t.args.length) { 101 | error("wrong number of arguments", t); 102 | } 103 | for (let i = 0; i < t.args.length; i++) { 104 | const argTy = typecheck(t.args[i], tyEnv); 105 | if (!subtype(argTy, funcTy.params[i].type)) { 106 | error("parameter type mismatch", t.args[i]); 107 | } 108 | } 109 | return funcTy.retType; 110 | } 111 | case "seq": 112 | typecheck(t.body, tyEnv); 113 | return typecheck(t.rest, tyEnv); 114 | case "const": { 115 | const ty = typecheck(t.init, tyEnv); 116 | const newTyEnv = { ...tyEnv, [t.name]: ty }; 117 | return typecheck(t.rest, newTyEnv); 118 | } 119 | case "objectNew": { 120 | const props = t.props.map( 121 | ({ name, term }) => ({ name, type: typecheck(term, tyEnv) }), 122 | ); 123 | return { tag: "Object", props }; 124 | } 125 | case "objectGet": { 126 | const objectTy = typecheck(t.obj, tyEnv); 127 | if (objectTy.tag !== "Object") error("object type expected", t.obj); 128 | const prop = objectTy.props.find((prop) => prop.name === t.propName); 129 | if (!prop) error(`unknown property name: ${t.propName}`, t); 130 | return prop.type; 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /book/typecheckers/sub_test.ts: -------------------------------------------------------------------------------- 1 | import test from "node:test"; 2 | import assert from "node:assert"; 3 | import { parseSub, typeShow } from "npm:tiny-ts-parser"; 4 | import { typecheck } from "./sub.ts"; 5 | 6 | function run(code: string) { 7 | return typecheck(parseSub(code), {}); 8 | } 9 | function ok(expected: string, code: string) { 10 | assert.equal(expected, typeShow(run(code))); 11 | } 12 | function ng(expected: RegExp, code: string) { 13 | assert.throws(() => { 14 | run(code); 15 | return true; 16 | }, expected); 17 | } 18 | 19 | test("record sub 1", () => 20 | ok( 21 | "number", 22 | ` 23 | type R1 = { a: number }; 24 | const f = (r: R1) => r.a; 25 | f({ a: 1, b: 1 }); 26 | `, 27 | )); 28 | test("record sub 2", () => 29 | ok( 30 | "number", 31 | ` 32 | type R1 = { a: number }; 33 | const f = (r: R1) => 1; 34 | const x = { a: 1, b: true }; 35 | f(x) 36 | `, 37 | )); 38 | test("record sub 3", () => 39 | ok( 40 | "number", 41 | ` 42 | type R1 = { a: number }; 43 | const f = (r: R1) => 1; 44 | f({ a: 1, b: true }); // type error in TS 45 | `, 46 | )); 47 | test("record sub 4", () => 48 | ok( 49 | "number", 50 | ` 51 | type R1 = { a: number }; 52 | type R2 = { a: number; b: number }; 53 | type R3 = { a: number; b: number; c: number }; 54 | const g = (f: (_: R2) => R2) => 1; 55 | const f = (_: R1) => ({ a: 1, b: 2, c: 3 }); 56 | g(f); 57 | `, 58 | )); 59 | test("record sub error 1", () => 60 | ng( 61 | /test.ts:7:5-7:6 parameter type mismatch/, 62 | ` 63 | type R1 = { a: number }; 64 | type R2 = { a: number; b: number }; 65 | type R3 = { a: number; b: number; c: number }; 66 | const g = (f: (_: R2) => R2) => 1; 67 | const f = (_: R3) => ({ a: 1, b: 2, c: 3 }); 68 | g(f); 69 | `, 70 | )); 71 | 72 | test("record sub error 2", () => 73 | ng( 74 | /test.ts:7:5-7:6 parameter type mismatch/, 75 | ` 76 | type R1 = { a: number }; 77 | type R2 = { a: number; b: number }; 78 | type R3 = { a: number; b: number; c: number }; 79 | const g = (f: (_: R2) => R2) => 1; 80 | const f = (_: R1) => ({ a: 1 }); 81 | g(f); 82 | `, 83 | )); 84 | /* 85 | test("if", () => ok("{ a: number; b: number }", ` 86 | true ? { a: 1, b: 2 } : { a: 2, b: 1 } 87 | `)); 88 | */ 89 | -------------------------------------------------------------------------------- /book/typecheckers/union.ts: -------------------------------------------------------------------------------- 1 | import { error } from "npm:tiny-ts-parser"; 2 | 3 | type Type = 4 | | { tag: "Boolean" } 5 | | { tag: "Number" } 6 | | { tag: "Func"; params: Param[]; retType: Type } 7 | | { tag: "Object"; props: PropertyType[] } 8 | | { tag: "TaggedUnion"; variants: VariantType[] }; 9 | 10 | type Param = { name: string; type: Type }; 11 | type PropertyType = { name: string; type: Type }; 12 | type VariantType = { tagLabel: string; props: PropertyType[] }; 13 | 14 | type Term = 15 | | { tag: "true" } 16 | | { tag: "false" } 17 | | { tag: "if"; cond: Term; thn: Term; els: Term } 18 | | { tag: "number"; n: number } 19 | | { tag: "add"; left: Term; right: Term } 20 | | { tag: "var"; name: string } 21 | | { tag: "func"; params: Param[]; body: Term } 22 | | { tag: "call"; func: Term; args: Term[] } 23 | | { tag: "seq"; body: Term; rest: Term } 24 | | { tag: "const"; name: string; init: Term; rest: Term } 25 | | { tag: "objectNew"; props: PropertyTerm[] } 26 | | { tag: "objectGet"; obj: Term; propName: string } 27 | | { tag: "taggedUnionNew"; tagLabel: string; props: PropertyTerm[]; as: Type } 28 | | { tag: "taggedUnionGet"; varName: string; clauses: VariantTerm[] }; 29 | 30 | type PropertyTerm = { name: string; term: Term }; 31 | type VariantTerm = { tagLabel: string; term: Term }; 32 | 33 | type TypeEnv = Record; 34 | 35 | function typeEq(ty1: Type, ty2: Type): boolean { 36 | switch (ty2.tag) { 37 | case "Boolean": 38 | return ty1.tag === "Boolean"; 39 | case "Number": 40 | return ty1.tag === "Number"; 41 | case "Func": { 42 | if (ty1.tag !== "Func") return false; 43 | if (ty1.params.length !== ty2.params.length) return false; 44 | for (let i = 0; i < ty1.params.length; i++) { 45 | if (!typeEq(ty1.params[i].type, ty2.params[i].type)) { 46 | return false; 47 | } 48 | } 49 | if (!typeEq(ty1.retType, ty2.retType)) return false; 50 | return true; 51 | } 52 | case "Object": { 53 | if (ty1.tag !== "Object") return false; 54 | if (ty1.props.length !== ty2.props.length) return false; 55 | for (const prop2 of ty2.props) { 56 | const prop1 = ty1.props.find((prop1) => prop1.name === prop2.name); 57 | if (!prop1) return false; 58 | if (!typeEq(prop1.type, prop2.type)) return false; 59 | } 60 | return true; 61 | } 62 | case "TaggedUnion": { 63 | if (ty1.tag !== "TaggedUnion") return false; 64 | if (ty1.variants.length !== ty2.variants.length) return false; 65 | for (const variant1 of ty1.variants) { 66 | const variant2 = ty2.variants.find( 67 | (variant2) => variant1.tagLabel === variant2.tagLabel, 68 | ); 69 | if (!variant2) return false; 70 | if (variant1.props.length !== variant2.props.length) return false; 71 | for (const prop1 of variant1.props) { 72 | const prop2 = variant2.props.find((prop2) => prop1.name === prop2.name); 73 | if (!prop2) return false; 74 | if (!typeEq(prop1.type, prop2.type)) return false; 75 | } 76 | } 77 | return true; 78 | } 79 | } 80 | } 81 | 82 | export function typecheck(t: Term, tyEnv: TypeEnv): Type { 83 | switch (t.tag) { 84 | case "true": 85 | return { tag: "Boolean" }; 86 | case "false": 87 | return { tag: "Boolean" }; 88 | case "if": { 89 | const condTy = typecheck(t.cond, tyEnv); 90 | if (condTy.tag !== "Boolean") error("boolean expected", t.cond); 91 | const thnTy = typecheck(t.thn, tyEnv); 92 | const elsTy = typecheck(t.els, tyEnv); 93 | if (!typeEq(thnTy, elsTy)) { 94 | error("then and else have different types", t); 95 | } 96 | return thnTy; 97 | } 98 | case "number": 99 | return { tag: "Number" }; 100 | case "add": { 101 | const leftTy = typecheck(t.left, tyEnv); 102 | if (leftTy.tag !== "Number") error("number expected", t.left); 103 | const rightTy = typecheck(t.right, tyEnv); 104 | if (rightTy.tag !== "Number") error("number expected", t.right); 105 | return { tag: "Number" }; 106 | } 107 | case "var": { 108 | if (tyEnv[t.name] === undefined) error(`unknown variable: ${t.name}`, t); 109 | return tyEnv[t.name]; 110 | } 111 | case "func": { 112 | const newTyEnv = { ...tyEnv }; 113 | for (const { name, type } of t.params) { 114 | newTyEnv[name] = type; 115 | } 116 | const retType = typecheck(t.body, newTyEnv); 117 | return { tag: "Func", params: t.params, retType }; 118 | } 119 | case "call": { 120 | const funcTy = typecheck(t.func, tyEnv); 121 | if (funcTy.tag !== "Func") error("function type expected", t.func); 122 | if (funcTy.params.length !== t.args.length) { 123 | error("wrong number of arguments", t); 124 | } 125 | for (let i = 0; i < t.args.length; i++) { 126 | const argTy = typecheck(t.args[i], tyEnv); 127 | if (!typeEq(argTy, funcTy.params[i].type)) { 128 | error("parameter type mismatch", t.args[i]); 129 | } 130 | } 131 | return funcTy.retType; 132 | } 133 | case "seq": 134 | typecheck(t.body, tyEnv); 135 | return typecheck(t.rest, tyEnv); 136 | case "const": { 137 | const ty = typecheck(t.init, tyEnv); 138 | const newTyEnv = { ...tyEnv, [t.name]: ty }; 139 | return typecheck(t.rest, newTyEnv); 140 | } 141 | case "objectNew": { 142 | const props = t.props.map( 143 | ({ name, term }) => ({ name, type: typecheck(term, tyEnv) }), 144 | ); 145 | return { tag: "Object", props }; 146 | } 147 | case "objectGet": { 148 | const objectTy = typecheck(t.obj, tyEnv); 149 | if (objectTy.tag !== "Object") error("object type expected", t.obj); 150 | const prop = objectTy.props.find((prop) => prop.name === t.propName); 151 | if (!prop) error(`unknown property name: ${t.propName}`, t); 152 | return prop.type; 153 | } 154 | case "taggedUnionNew": { 155 | const asTy = t.as; 156 | if (asTy.tag !== "TaggedUnion") { 157 | error(`"as" must have a tagged union type`, t); 158 | } 159 | const variant = asTy.variants.find( 160 | (variant) => variant.tagLabel === t.tagLabel, 161 | ); 162 | if (!variant) error(`unknown variant tag: ${t.tagLabel}`, t); 163 | for (const prop1 of t.props) { 164 | const prop2 = variant.props.find((prop2) => prop1.name === prop2.name); 165 | if (!prop2) error(`unknown property: ${prop1.name}`, t); 166 | const actualTy = typecheck(prop1.term, tyEnv); 167 | if (!typeEq(actualTy, prop2.type)) { 168 | error("tagged union's term has a wrong type", prop1.term); 169 | } 170 | } 171 | return t.as; 172 | } 173 | case "taggedUnionGet": { 174 | const variantTy = tyEnv[t.varName]; 175 | if (variantTy.tag !== "TaggedUnion") { 176 | error(`variable ${t.varName} must have a tagged union type`, t); 177 | } 178 | let retTy: Type | null = null; 179 | for (const clause of t.clauses) { 180 | const variant = variantTy.variants.find( 181 | (variant) => variant.tagLabel === clause.tagLabel, 182 | ); 183 | if (!variant) { 184 | error(`tagged union type has no case: ${clause.tagLabel}`, clause.term); 185 | } 186 | const localTy: Type = { tag: "Object", props: variant.props }; 187 | const newTyEnv = { ...tyEnv, [t.varName]: localTy }; 188 | const clauseTy = typecheck(clause.term, newTyEnv); 189 | if (retTy) { 190 | if (!typeEq(retTy, clauseTy)) { 191 | error("clauses has different types", clause.term); 192 | } 193 | } else { 194 | retTy = clauseTy; 195 | } 196 | } 197 | if (variantTy.variants.length !== t.clauses.length) { 198 | error("switch case is not exhaustive", t); 199 | } 200 | return retTy!; 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /book/typecheckers/union_test.ts: -------------------------------------------------------------------------------- 1 | import test from "node:test"; 2 | import assert from "node:assert"; 3 | import { parseTaggedUnion, typeShow } from "npm:tiny-ts-parser"; 4 | import { typecheck } from "./union.ts"; 5 | 6 | function run(code: string) { 7 | return typecheck(parseTaggedUnion(code), {}); 8 | } 9 | function ok(expected: string, code: string) { 10 | assert.equal(expected, typeShow(run(code))); 11 | } 12 | function ng(expected: RegExp, code: string) { 13 | assert.throws(() => { 14 | run(code); 15 | return true; 16 | }, expected); 17 | } 18 | 19 | test("tagged union", () => 20 | ok( 21 | "number", 22 | ` 23 | type NumOrBoolean = { tag: "num", num: number } | { tag: "bool", bool: boolean }; 24 | const v = { tag: "num", num: 42 } satisfies { tag: "num", num: number } | { tag: "bool", bool: boolean }; 25 | switch (v.tag) { 26 | case "num": { 27 | v.num + 1; 28 | } 29 | case "bool": { 30 | v.bool ? 1 : 2; 31 | } 32 | } 33 | `, 34 | )); 35 | test("tagged union error 1", () => 36 | ng( 37 | /test.ts:3:32-3:36 tagged union's term has a wrong type/, 38 | ` 39 | type NumOrBoolean = { tag: "num", num: number } | { tag: "bool", bool: boolean }; 40 | const v = { tag: "num", num: true } satisfies NumOrBoolean; 41 | 1; 42 | `, 43 | )); 44 | test("tagged union error 2", () => 45 | ng( 46 | /test.ts:3:13-3:62 unknown property: bool/, 47 | ` 48 | type NumOrBoolean = { tag: "num", num: number } | { tag: "bool", bool: boolean }; 49 | const v = { tag: "num", bool: true } satisfies NumOrBoolean; 50 | 1; 51 | `, 52 | )); 53 | test("tagged union error 3", () => 54 | ng( 55 | /test.ts:3:3-7:4 variable v must have a tagged union type/, 56 | ` 57 | const v = 1; 58 | switch (v.tag) { 59 | case "num": { 60 | v.val + 1; 61 | } 62 | } 63 | `, 64 | )); 65 | test("tagged union error 4", () => 66 | ng( 67 | /test.ts:12:7-12:20 tagged union type has no case: unknown/, 68 | ` 69 | type NumOrBoolean = { tag: "num", val: number } | { tag: "bool", val: boolean }; 70 | const v = { tag: "num", val: 42 } satisfies NumOrBoolean; 71 | switch (v.tag) { 72 | case "num": { 73 | v.val + 1; 74 | } 75 | case "bool": { 76 | v.val ? 1 : 2; 77 | } 78 | case "unknown": { 79 | v.val ? 1 : 2; 80 | } 81 | } 82 | `, 83 | )); 84 | test("tagged union error 5", () => 85 | ng( 86 | /test.ts:9:16-9:21 clauses has different types/, 87 | ` 88 | type NumOrBoolean = { tag: "num", val: number } | { tag: "bool", val: boolean }; 89 | const dispatch = (v: NumOrBoolean) => { 90 | switch (v.tag) { 91 | case "num": { 92 | return v.val; 93 | } 94 | case "bool": { 95 | return v.val; 96 | } 97 | } 98 | }; 99 | `, 100 | )); 101 | -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "fmt": { 3 | "options": { 4 | "lineWidth": 120, 5 | "semiColons": true 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4", 3 | "specifiers": { 4 | "npm:@types/node@*": "22.12.0", 5 | "npm:@typescript-eslint/typescript-estree@*": "8.21.0_typescript@5.4.5", 6 | "npm:@typescript-eslint/typescript-estree@7.11.0": "7.11.0", 7 | "npm:tiny-ts-parser@*": "0.1.3" 8 | }, 9 | "npm": { 10 | "@nodelib/fs.scandir@2.1.5": { 11 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 12 | "dependencies": [ 13 | "@nodelib/fs.stat", 14 | "run-parallel" 15 | ] 16 | }, 17 | "@nodelib/fs.stat@2.0.5": { 18 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" 19 | }, 20 | "@nodelib/fs.walk@1.2.8": { 21 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 22 | "dependencies": [ 23 | "@nodelib/fs.scandir", 24 | "fastq" 25 | ] 26 | }, 27 | "@types/node@22.12.0": { 28 | "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==", 29 | "dependencies": [ 30 | "undici-types" 31 | ] 32 | }, 33 | "@typescript-eslint/types@7.11.0": { 34 | "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==" 35 | }, 36 | "@typescript-eslint/types@7.16.0": { 37 | "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==" 38 | }, 39 | "@typescript-eslint/types@8.21.0": { 40 | "integrity": "sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A==" 41 | }, 42 | "@typescript-eslint/types@8.24.0": { 43 | "integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==" 44 | }, 45 | "@typescript-eslint/types@8.30.1": { 46 | "integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==" 47 | }, 48 | "@typescript-eslint/typescript-estree@7.11.0": { 49 | "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==", 50 | "dependencies": [ 51 | "@typescript-eslint/types@7.11.0", 52 | "@typescript-eslint/visitor-keys@7.11.0", 53 | "debug", 54 | "globby", 55 | "is-glob", 56 | "minimatch", 57 | "semver", 58 | "ts-api-utils@1.3.0_typescript@5.4.5" 59 | ] 60 | }, 61 | "@typescript-eslint/typescript-estree@7.16.0": { 62 | "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", 63 | "dependencies": [ 64 | "@typescript-eslint/types@7.16.0", 65 | "@typescript-eslint/visitor-keys@7.16.0", 66 | "debug", 67 | "globby", 68 | "is-glob", 69 | "minimatch", 70 | "semver", 71 | "ts-api-utils@1.3.0_typescript@5.4.5" 72 | ] 73 | }, 74 | "@typescript-eslint/typescript-estree@8.21.0_typescript@5.4.5": { 75 | "integrity": "sha512-x+aeKh/AjAArSauz0GiQZsjT8ciadNMHdkUSwBB9Z6PrKc/4knM4g3UfHml6oDJmKC88a6//cdxnO/+P2LkMcg==", 76 | "dependencies": [ 77 | "@typescript-eslint/types@8.21.0", 78 | "@typescript-eslint/visitor-keys@8.21.0", 79 | "debug", 80 | "fast-glob", 81 | "is-glob", 82 | "minimatch", 83 | "semver", 84 | "ts-api-utils@2.1.0_typescript@5.4.5", 85 | "typescript" 86 | ] 87 | }, 88 | "@typescript-eslint/typescript-estree@8.24.0_typescript@5.4.5": { 89 | "integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==", 90 | "dependencies": [ 91 | "@typescript-eslint/types@8.24.0", 92 | "@typescript-eslint/visitor-keys@8.24.0", 93 | "debug", 94 | "fast-glob", 95 | "is-glob", 96 | "minimatch", 97 | "semver", 98 | "ts-api-utils@2.0.1_typescript@5.4.5", 99 | "typescript" 100 | ] 101 | }, 102 | "@typescript-eslint/typescript-estree@8.30.1_typescript@5.4.5": { 103 | "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", 104 | "dependencies": [ 105 | "@typescript-eslint/types@8.30.1", 106 | "@typescript-eslint/visitor-keys@8.30.1", 107 | "debug", 108 | "fast-glob", 109 | "is-glob", 110 | "minimatch", 111 | "semver", 112 | "ts-api-utils@2.1.0_typescript@5.4.5", 113 | "typescript" 114 | ] 115 | }, 116 | "@typescript-eslint/visitor-keys@7.11.0": { 117 | "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==", 118 | "dependencies": [ 119 | "@typescript-eslint/types@7.11.0", 120 | "eslint-visitor-keys@3.4.3" 121 | ] 122 | }, 123 | "@typescript-eslint/visitor-keys@7.16.0": { 124 | "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", 125 | "dependencies": [ 126 | "@typescript-eslint/types@7.16.0", 127 | "eslint-visitor-keys@3.4.3" 128 | ] 129 | }, 130 | "@typescript-eslint/visitor-keys@8.21.0": { 131 | "integrity": "sha512-BkLMNpdV6prozk8LlyK/SOoWLmUFi+ZD+pcqti9ILCbVvHGk1ui1g4jJOc2WDLaeExz2qWwojxlPce5PljcT3w==", 132 | "dependencies": [ 133 | "@typescript-eslint/types@8.21.0", 134 | "eslint-visitor-keys@4.2.0" 135 | ] 136 | }, 137 | "@typescript-eslint/visitor-keys@8.24.0": { 138 | "integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==", 139 | "dependencies": [ 140 | "@typescript-eslint/types@8.24.0", 141 | "eslint-visitor-keys@4.2.0" 142 | ] 143 | }, 144 | "@typescript-eslint/visitor-keys@8.30.1": { 145 | "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", 146 | "dependencies": [ 147 | "@typescript-eslint/types@8.30.1", 148 | "eslint-visitor-keys@4.2.0" 149 | ] 150 | }, 151 | "array-union@2.1.0": { 152 | "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" 153 | }, 154 | "balanced-match@1.0.2": { 155 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 156 | }, 157 | "brace-expansion@2.0.1": { 158 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 159 | "dependencies": [ 160 | "balanced-match" 161 | ] 162 | }, 163 | "braces@3.0.3": { 164 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 165 | "dependencies": [ 166 | "fill-range" 167 | ] 168 | }, 169 | "debug@4.3.5": { 170 | "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", 171 | "dependencies": [ 172 | "ms" 173 | ] 174 | }, 175 | "dir-glob@3.0.1": { 176 | "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", 177 | "dependencies": [ 178 | "path-type" 179 | ] 180 | }, 181 | "eslint-visitor-keys@3.4.3": { 182 | "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==" 183 | }, 184 | "eslint-visitor-keys@4.2.0": { 185 | "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==" 186 | }, 187 | "fast-glob@3.3.2": { 188 | "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", 189 | "dependencies": [ 190 | "@nodelib/fs.stat", 191 | "@nodelib/fs.walk", 192 | "glob-parent", 193 | "merge2", 194 | "micromatch" 195 | ] 196 | }, 197 | "fastq@1.17.1": { 198 | "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", 199 | "dependencies": [ 200 | "reusify" 201 | ] 202 | }, 203 | "fill-range@7.1.1": { 204 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 205 | "dependencies": [ 206 | "to-regex-range" 207 | ] 208 | }, 209 | "glob-parent@5.1.2": { 210 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 211 | "dependencies": [ 212 | "is-glob" 213 | ] 214 | }, 215 | "globby@11.1.0": { 216 | "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", 217 | "dependencies": [ 218 | "array-union", 219 | "dir-glob", 220 | "fast-glob", 221 | "ignore", 222 | "merge2", 223 | "slash" 224 | ] 225 | }, 226 | "ignore@5.3.1": { 227 | "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==" 228 | }, 229 | "is-extglob@2.1.1": { 230 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" 231 | }, 232 | "is-glob@4.0.3": { 233 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 234 | "dependencies": [ 235 | "is-extglob" 236 | ] 237 | }, 238 | "is-number@7.0.0": { 239 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" 240 | }, 241 | "merge2@1.4.1": { 242 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" 243 | }, 244 | "micromatch@4.0.7": { 245 | "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", 246 | "dependencies": [ 247 | "braces", 248 | "picomatch" 249 | ] 250 | }, 251 | "minimatch@9.0.5": { 252 | "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", 253 | "dependencies": [ 254 | "brace-expansion" 255 | ] 256 | }, 257 | "ms@2.1.2": { 258 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 259 | }, 260 | "path-type@4.0.0": { 261 | "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" 262 | }, 263 | "picomatch@2.3.1": { 264 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" 265 | }, 266 | "queue-microtask@1.2.3": { 267 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" 268 | }, 269 | "reusify@1.0.4": { 270 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" 271 | }, 272 | "run-parallel@1.2.0": { 273 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 274 | "dependencies": [ 275 | "queue-microtask" 276 | ] 277 | }, 278 | "semver@7.6.2": { 279 | "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==" 280 | }, 281 | "slash@3.0.0": { 282 | "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" 283 | }, 284 | "tiny-ts-parser@0.1.3": { 285 | "integrity": "sha512-EKXKcEBJ4i0bOVwJdr2cbjlvjvdlQPD46evolIIeIyE7dzldi2lWA16WAFyL0ZspV/7t2vmDt+CEub6UqCpQ3w==", 286 | "dependencies": [ 287 | "@typescript-eslint/typescript-estree@8.30.1_typescript@5.4.5" 288 | ] 289 | }, 290 | "to-regex-range@5.0.1": { 291 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 292 | "dependencies": [ 293 | "is-number" 294 | ] 295 | }, 296 | "ts-api-utils@1.3.0_typescript@5.4.5": { 297 | "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", 298 | "dependencies": [ 299 | "typescript" 300 | ] 301 | }, 302 | "ts-api-utils@2.0.1_typescript@5.4.5": { 303 | "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", 304 | "dependencies": [ 305 | "typescript" 306 | ] 307 | }, 308 | "ts-api-utils@2.1.0_typescript@5.4.5": { 309 | "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", 310 | "dependencies": [ 311 | "typescript" 312 | ] 313 | }, 314 | "typescript@5.4.5": { 315 | "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==" 316 | }, 317 | "undici-types@6.20.0": { 318 | "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" 319 | } 320 | }, 321 | "remote": { 322 | "https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", 323 | "https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834", 324 | "https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293", 325 | "https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7", 326 | "https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74", 327 | "https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd", 328 | "https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff", 329 | "https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46", 330 | "https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b", 331 | "https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c", 332 | "https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491", 333 | "https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68", 334 | "https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3", 335 | "https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7", 336 | "https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29", 337 | "https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a", 338 | "https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a", 339 | "https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8", 340 | "https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693", 341 | "https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31", 342 | "https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5", 343 | "https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8", 344 | "https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb", 345 | "https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917", 346 | "https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47", 347 | "https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68", 348 | "https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3", 349 | "https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73", 350 | "https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19", 351 | "https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5", 352 | "https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6", 353 | "https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2", 354 | "https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e" 355 | } 356 | } 357 | --------------------------------------------------------------------------------