13 | Welcome to Cat. It is a programming language.
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "type-inference",
3 | "version": "1.1.0",
4 | "description": "A type inference library written in TypeScript",
5 | "main": "type-inference.js",
6 | "homepage": "https://github.com/cdiggins/type-inference",
7 | "bugs": "https://github.com/cdiggins/type-inference/issues",
8 | "license": "MIT",
9 | "author": {
10 | "name": "Christopher Diggins",
11 | "email": "cdiggins@gmail.com"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/cdiggins/type-inference.git"
16 | },
17 | "scripts": {
18 | "build": "tsc"
19 | },
20 | "dependencies": {
21 | "myna-parser": ">=2.5"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/cat-library.ts:
--------------------------------------------------------------------------------
1 | // Standard operations
2 | export const catStdOps = {
3 | papply : "swap quote swap compose",
4 | dip : "swap quote compose apply",
5 | dipd : "swap [dip] dip",
6 | popd : "[pop] dip",
7 | pop2 : "pop pop",
8 | pop3 : "pop pop pop",
9 | dupd : "[dup] dip",
10 | dupdd : "[dup] dipd",
11 | swapd : "[swap] dip",
12 | swapdd : "[swap] dipd",
13 | rollup : "swap swapd",
14 | rolldown : "swapd swap",
15 | dup3 : "dup dup",
16 | dup4 : "dup3 dup",
17 | dup5 : "dup4 dup",
18 | dup6 : "dup5 dup",
19 | dup7 : "dup6 dup",
20 | dup8 : "dup7 dup",
21 | test : "dup [[0] dip apply []] dip",
22 | }
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible Node.js debug attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Launch Program",
11 | "program": "${file}",
12 | "cwd": "${workspaceRoot}",
13 | "outFiles": ["${workspaceRoot}/build/**/*.js"],
14 | "sourceMaps": true
15 | },
16 | {
17 | "type": "node",
18 | "request": "attach",
19 | "name": "Attach to Process",
20 | "port": 5858,
21 | "outFiles": [],
22 | "timeout": 30000,
23 | "sourceMaps": true
24 | }
25 | ]
26 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Christopher Diggins
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 |
--------------------------------------------------------------------------------
/lambda-calculus-to-cat.ts:
--------------------------------------------------------------------------------
1 | // Conversion Rules from Lambda Calculus to Lambda Cat
2 | //
3 | // x T => T @x apply
4 | // T x => @x T apply
5 | // \x.T => [\x T]
6 |
7 | import { Redex, RedexAbstraction, RedexApplication, RedexName, RedexValue } from "./lambda-calculus";
8 | import { CatExpr, CatParam, CatQuotation, CatInstruction, CatVar, CatConstant } from "./cat-parser";
9 | import { removeVars } from "./cat-lambda";
10 |
11 | // Converts a lambda calculus term to an array of Cat expressions.
12 | export function lambdaToCat(x: Redex): CatExpr[] {
13 | var r: CatExpr[] = [];
14 | if (x instanceof RedexAbstraction) {
15 | r.push(new CatQuotation([
16 | new CatParam(x.param),
17 | ...lambdaToCat(x.body)]));
18 | }
19 | else if (x instanceof RedexApplication) {
20 | r.push(...lambdaToCat(x.args));
21 | r.push(...lambdaToCat(x.func));
22 | r.push(new CatInstruction('apply'));
23 | }
24 | else if (x instanceof RedexName) {
25 | r.push(new CatVar(x.name));
26 | }
27 | else if (x instanceof RedexValue) {
28 | r.push(new CatConstant(x.value));
29 | }
30 | else {
31 | throw new Error("Unrecognized Redex " + x);
32 | }
33 | return r;
34 | }
35 |
36 | export function lambdaToPureCat(x: Redex): CatExpr[] {
37 | return removeVars(lambdaToCat(x));
38 | }
--------------------------------------------------------------------------------
/type-parser.ts:
--------------------------------------------------------------------------------
1 | import { Type, typeConstant, typeVariable, typeArray } from "./type-system";
2 | import { Myna as m } from "myna-parser"
3 |
4 | // Defines syntax parsers for type expression, the lambda calculus, and Cat
5 | function registerGrammars()
6 | {
7 | // A simple grammar for parsing type expressions
8 | var typeGrammar = new function()
9 | {
10 | var _this = this;
11 | this.typeExprRec = m.delay(() => { return _this.typeExpr});
12 | this.typeList = m.guardedSeq('(', m.ws, this.typeExprRec.ws.zeroOrMore, ')').ast;
13 | this.typeVar = m.guardedSeq("'", m.identifier).ast;
14 | this.typeConstant = m.identifier.or(m.digits).or("->").or("*").or("[]").ast;
15 | this.typeExpr = m.choice(this.typeList, this.typeVar, this.typeConstant).ast;
16 | }
17 | m.registerGrammar('type', typeGrammar, typeGrammar.typeExpr);
18 |
19 | }
20 |
21 | registerGrammars();
22 |
23 | export const typeParser = m.parsers['type'];
24 |
25 | export function parseType(input:string) : Type {
26 | var ast = typeParser(input);
27 | if (ast.end != input.length)
28 | throw new Error("Only part of input was consumed");
29 | return astToType(ast);
30 | }
31 |
32 | function astToType(ast) : Type {
33 | if (!ast)
34 | return null;
35 | switch (ast.name)
36 | {
37 | case "typeVar":
38 | return typeVariable(ast.allText.substr(1));
39 | case "typeConstant":
40 | return typeConstant(ast.allText);
41 | case "typeList":
42 | return typeArray(ast.children.map(astToType));
43 | case "typeExpr":
44 | if (ast.children.length != 1)
45 | throw new Error("Expected only one child of node, not " + ast.children.length);
46 | return astToType(ast.children[0]);
47 | default:
48 | throw new Error("Unrecognized type expression: " + ast.name);
49 | }
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/cat-types.ts:
--------------------------------------------------------------------------------
1 | import { Myna as m } from "./node_modules/myna-parser/myna";
2 | import { Type, quotation, typeConstant, TypeArray, idFunction, composeFunctionChain, composeFunctionChainReverse } from "./type-system";
3 | import { parseType, typeParser } from './type-parser';
4 | import { catParser } from "./cat-parser";
5 | import { catStdOps } from "./cat-library";
6 |
7 | export var verbose: boolean = true;
8 |
9 | // The types of the core Cat
10 | export const catTypes = {
11 | apply : "((('a -> 'b) 'a) -> 'b)",
12 | call : "(((('a 's) -> ('b 's)) ('a 's)) -> ('b 's))",
13 | compose : "((('b -> 'c) (('a -> 'b) 'd)) -> (('a -> 'c) 'd))",
14 | quote : "(('a 'b) -> (('c -> ('a 'c)) 'b))",
15 | dup : "(('a 'b) -> ('a ('a 'b)))",
16 | swap : "(('a ('b 'c)) -> ('b ('a 'c)))",
17 | pop : "(('a 'b) -> 'b)",
18 | id : "('a -> 'a)",
19 | };
20 |
21 | // Converted into type expressions (so we don't reparse each time)
22 | export type TypeLookup = { [op: string] : Type };
23 | export const catTypesParsed : TypeLookup = { }
24 |
25 | // Parse the types
26 | for (let k in catTypes) {
27 | const t = parseType(catTypes[k]);
28 | if (verbose)
29 | console.log(k + " : " + t);
30 | catTypesParsed[k] = t;
31 | }
32 |
33 | // Compute the standard op types
34 | for (let op in catStdOps) {
35 | const def = catStdOps[op];
36 | const t = inferCatType(def);
37 | if (verbose)
38 | console.log(op + " = " + def + " : " + t);
39 | catTypesParsed[op] = t;
40 | }
41 |
42 | function catTypeFromAst(ast: m.AstNode) : TypeArray {
43 | switch (ast.name) {
44 | case "integer":
45 | return quotation(typeConstant('Num'));
46 | case "true":
47 | case "false":
48 | return quotation(typeConstant('Bool'));
49 | case "identifier": {
50 | if (!(ast.allText in catTypesParsed))
51 | throw new Error("Could not find type for term: " + ast.allText);
52 | return catTypesParsed[ast.allText] as TypeArray;
53 | }
54 | case "quotation": {
55 | var innerType = ast.children.length > 0
56 | ? catTypeFromAst(ast.children[0])
57 | : idFunction()
58 | return quotation(innerType);
59 | }
60 | case "terms": {
61 | var types = ast.children.map(catTypeFromAst);
62 | return composeFunctionChainReverse(types);
63 | }
64 | default:
65 | throw new Error("Could not figure out function type");
66 | }
67 | }
68 |
69 | export function inferCatType(s:string) : TypeArray {
70 | var ast = catParser(s);
71 | if (ast.allText.length != s.length)
72 | throw new Error("Could not parse the entire term: " + s);
73 | return catTypeFromAst(ast);
74 | }
--------------------------------------------------------------------------------
/scoped-hm-type-inference.ts:
--------------------------------------------------------------------------------
1 | import { Type, Unifier, isFunctionType, TypeVariable, functionType, functionOutput, functionInput, trace, typeVariable, TypeArray } from "./type-system";
2 |
3 | //=========================================================
4 | // A simple helper class for implementing scoped programming languages with names like the lambda calculus.
5 | // This class is more intended as an example of usage of the algorithm than for use in production code
6 | export class ScopedTypeInferenceEngine
7 | {
8 | id : number = 0;
9 | names : string[] = [];
10 | types : Type[] = [];
11 | unifier : Unifier = new Unifier();
12 |
13 | applyFunction(t:Type, args:Type) : Type
14 | {
15 | if (!isFunctionType(t))
16 | {
17 | // Only variables and functions can be applied
18 | if (!(t instanceof TypeVariable))
19 | throw new Error("Type is neither a function type or a type variable: " + t);
20 |
21 | // Generate a new function type
22 | var newInputType = this.introduceVariable(t.name + "_i");
23 | var newOutputType = this.introduceVariable(t.name + "_o");
24 | var fxnType = functionType(newInputType, newOutputType);
25 | fxnType.computeParameters();
26 |
27 | // Unify the new function type with the old variable
28 | this.unifier.unifyTypes(t, fxnType);
29 | t = fxnType;
30 | }
31 |
32 | // What is the input of the function: unify with the argument
33 | var input = functionInput(t);
34 | var output = functionOutput(t);
35 | if (trace) this.logState("before function application");
36 | this.unifier.unifyTypes(input, args);
37 | if (trace) this.logState("after function application");
38 | //return this.unifier.getUnifiedType(output, [], {});
39 | return output;
40 | }
41 |
42 | introduceVariable(name:string) : TypeVariable{
43 | var t = typeVariable(name + '$' + this.id++);
44 | this.names.push(name);
45 | this.types.push(t);
46 | return t;
47 | }
48 |
49 | lookupOrIntroduceVariable(name:string) : Type {
50 | var n = this.indexOfVariable(name);
51 | return (n < 0) ? this.introduceVariable(name) : this.getUnifiedType(this.types[n]);
52 | }
53 |
54 | assignVariable(name:string, t:Type) : Type {
55 | return this.unifier.unifyTypes(this.lookupVariable(name), t);
56 | }
57 |
58 | indexOfVariable(name:string) : number {
59 | return this.names.lastIndexOf(name);
60 | }
61 |
62 | lookupVariable(name:string) : Type {
63 | var n = this.indexOfVariable(name);
64 | if (n < 0) throw new Error("Could not find variable: " + name);
65 | return this.getUnifiedType(this.types[n]);
66 | }
67 |
68 | getUnifiedType(t:Type) : Type {
69 | var r = this.unifier.getUnifiedType(t, [], {});
70 | if (r instanceof TypeArray)
71 | r.computeParameters();
72 | return r;
73 | }
74 |
75 | popVariable() {
76 | this.types.pop();
77 | this.names.pop();
78 | }
79 |
80 | get state() : string {
81 | var r = [];
82 | for (var i=0; i < this.types.length; ++i) {
83 | var t = this.types[i];
84 | var n = this.names[i]
85 | var u = this.getUnifiedType(t);
86 | r.push(n + " : " + t + " = " + u);
87 | }
88 | return r.join("\n");
89 | }
90 |
91 | logState(msg:string = "") {
92 | console.log("Inference engine state " + msg);
93 | console.log(this.state);
94 | console.log(this.unifier.state);
95 | }
96 | }
97 |
98 |
--------------------------------------------------------------------------------
/cat-rewrite.ts:
--------------------------------------------------------------------------------
1 |
2 | import { parseCat, CatExpr, CatQuotation, CatInstruction, CatParam, CatVar, CatCapture, CatConstant } from './cat-parser';
3 |
4 | // Rewriting rules
5 | export const catRewriteRuleDefs = {
6 | "id" : "",
7 | "swap swap" : "",
8 | "dup pop" : "",
9 | "quote apply": "",
10 | "quote pop": "",
11 | "$1 pop": "",
12 | "$1 dup": "$1 $1",
13 | "$1 $2 swap": "$2 $1",
14 | "$1 quote": "[$1]",
15 | "[$1] apply": "$1",
16 | "[$1] [$2] compose": "[$1 $2]",
17 | "[$1] [$2] papply": "[[$1] $2]",
18 | "[$1] dip [$2] dip": "[$1 $2] dip",
19 | "$1 $2 dip": "$2 apply $1",
20 | }
21 |
22 | class RewriteRule {
23 | constructor(
24 | public readonly src: CatExpr[],
25 | public readonly target: CatExpr[])
26 | { }
27 | }
28 |
29 | export const catRewriteRules: RewriteRule[] =
30 | Object.keys(catRewriteRuleDefs).map(
31 | k => new RewriteRule(parseCat(k), parseCat(catRewriteRuleDefs[k]))
32 | );
33 |
34 | interface Matches {
35 | [name: string] : CatExpr;
36 | }
37 |
38 | function isValue(expr: CatExpr) {
39 | return expr instanceof CatQuotation || expr instanceof CatVar || expr instanceof CatConstant;
40 | }
41 |
42 | // Given a Cat expression representing a pattern, returns true if the array of expressions matches
43 | // from the given position. All captures are added to the matches object.
44 | function matchPattern(expr:CatExpr[], pattern:CatExpr[], matches: Matches, pos:number=0) : boolean {
45 | // Check that there are enough terms to match the pattern
46 | if (pos + pattern.length > expr.length)
47 | return false;
48 | for (var i=0; i < pattern.length; ++i) {
49 | const p = pattern[i];
50 | const x = expr[i + pos];
51 | if (p instanceof CatCapture) {
52 | if (p.name in matches)
53 | throw new Error("Match variable repeated twice: " + p.name);
54 | if (!isValue(x))
55 | return false; // Can't match non-values
56 | matches[p.name] = x;
57 | }
58 | else if (p instanceof CatQuotation && x instanceof CatQuotation) {
59 | // Recurse into patterns
60 | if (!matchPattern(x.terms, p.terms, matches))
61 | return false;
62 | }
63 | else if (p instanceof CatConstant && x instanceof CatConstant) {
64 | if (p.value !== x.value)
65 | return false;
66 | }
67 | else if (p instanceof CatInstruction && x instanceof CatInstruction) {
68 | if (p.name != x.name)
69 | return false;
70 | }
71 | }
72 | return true;
73 | }
74 |
75 | function applyMatches(x: CatExpr, matches: Matches) : CatExpr {
76 | if (x instanceof CatCapture) {
77 | if (!(x.name in matches))
78 | throw new Error("Match could not be found");
79 | return matches[x.name];
80 | }
81 | else if (x instanceof CatQuotation) {
82 | return new CatQuotation(x.terms.map(t => applyMatches(t, matches)));
83 | }
84 | else {
85 | return x;
86 | }
87 | }
88 |
89 | function rewrite(expr: CatExpr[], targetPattern: CatExpr[], matches: Matches, pos: number, length: number): CatExpr[] {
90 | const newPattern = targetPattern.map(x => applyMatches(x, matches));
91 | var r = [...expr];
92 | r.splice(pos, length, ...newPattern);
93 | return r;
94 | }
95 |
96 | // If the pattern matches at the specified position, returns a rewritten expression, or the same expression otherwise
97 | function rewriteIfMatch(expr:CatExpr[], pos:number, rule: RewriteRule) : CatExpr[] {
98 | var matches: Matches = {};
99 | if (matchPattern(expr, rule.src, matches, pos)) {
100 | return rewrite(expr, rule.target, matches, pos, rule.src.length);
101 | }
102 | return null;
103 | }
104 |
105 | // Keeps applying rewrite rules as possible
106 | function applyRewriteRules(expr:CatExpr[]): CatExpr {
107 | var i=0;
108 | while (i < expr.length) {
109 | for (var r of catRewriteRules) {
110 | var tmp = rewriteIfMatch(expr, i, r);
111 | if (tmp != null) {
112 | expr = tmp;
113 | // We back up three places, to look for new patterns that might have emerged
114 | i -= 3;
115 | // Don't back up too much
116 | if (i < 0) i = 0;
117 | }
118 | else {
119 | i++;
120 | }
121 | }
122 | }
123 | return expr;
124 | }
--------------------------------------------------------------------------------
/cat-parser.ts:
--------------------------------------------------------------------------------
1 | // A parser for a super-set of the Cat language called Lambda-Cat
2 | // which includes variables. In Lambda-Cat the introduction of a variable is denoted by \x.
3 | //
4 | // This is inspired by the work of Brent Kerby, but is modified http://tunes.org/~iepos/joy.html
5 | //
6 | // Copyright 2017 by Christopher Diggins
7 | // Licensed under the MIT License
8 |
9 | import { Myna as m } from "myna-parser";
10 |
11 | // Defines a Myna grammar for parsing Cat expressions that support the introduction and usage of scoped variables.
12 | export const catGrammar = new function()
13 | {
14 | var _this = this;
15 | this.identifier = m.identifier.ast;
16 | this.param = m.guardedSeq('\\', this.identifier).ast;
17 | this.var = m.guardedSeq('@', this.identifier).ast;
18 | this.match = m.guardedSeq('$', this.identifier).ast;
19 | this.integer = m.integer.ast;
20 | this.true = m.keyword("true").ast;
21 | this.false = m.keyword("false").ast;
22 | this.recTerm = m.delay(() => { return _this.term; });
23 | this.recTerms = m.delay(() => { return _this.terms; });
24 | this.quotation = m.guardedSeq('[', m.ws, this.recTerms, m.ws, ']').ast;
25 | this.term = m.choice(this.param, this.var, this.quotation, this.integer, this.true, this.false, this.identifier);
26 | this.terms = m.ws.then(this.term.ws.zeroOrMore).ast;
27 | }
28 |
29 | m.registerGrammar('cat', catGrammar, catGrammar.terms);
30 | export const catParser = m.parsers['cat'];
31 |
32 | export function parseCat(s:string) : CatExpr[] {
33 | var ast = catParser(s);
34 | if (ast.end != s.length)
35 | throw new Error("Whole input was not parsed");
36 | return ast.children.map(astToCat);
37 | }
38 |
39 | export function astToCat(ast:m.AstNode) : CatExpr {
40 | switch (ast.name) {
41 | case "param":
42 | return new CatParam(ast.allText.slice(1));
43 | case "identifier":
44 | return new CatInstruction(ast.allText);
45 | case "var":
46 | return new CatVar(ast.allText.slice(1));
47 | case "match":
48 | return new CatCapture(ast.allText.slice(1));
49 | case "true":
50 | return new CatConstant(true);
51 | case "false":
52 | return new CatConstant(false);
53 | case "integer":
54 | return new CatConstant(parseInt(ast.allText));
55 | case "quotation":
56 | return new CatQuotation(ast.children.map(astToCat));
57 | default:
58 | throw new Error("Unrecognized AST type " + ast.name);
59 | }
60 | }
61 |
62 | export class CatExpr {
63 | }
64 |
65 | export class CatParam extends CatExpr {
66 | constructor(public readonly name:string) {
67 | super();
68 | }
69 | public toString() : string {
70 | return '\\' + this.name;
71 | }
72 | }
73 |
74 | export class CatVar extends CatExpr {
75 | constructor(public readonly name:string) {
76 | super();
77 | }
78 | public toString() : string {
79 | return '@' + this.name;
80 | }
81 | }
82 |
83 | export class CatCapture extends CatExpr {
84 | constructor(public readonly name:string) {
85 | super();
86 | }
87 | public toString() : string {
88 | return '%' + this.name;
89 | }
90 | }
91 |
92 | export class CatQuotation extends CatExpr {
93 | constructor(public readonly terms:CatExpr[]) {
94 | super();
95 | }
96 | public toString() : string {
97 | return "[" + this.terms.join(" ") + "]";
98 | }
99 | splice(pos: number, delCount: number, ...terms: CatExpr[]): CatQuotation {
100 | var r = this.terms.slice();
101 | r.splice(pos, delCount, ...terms);
102 | return new CatQuotation(r);
103 | }
104 | delete(pos: number, delCount: number): CatQuotation {
105 | return this.splice(pos, delCount);
106 | }
107 | insert(pos: number, ...terms: CatExpr[]): CatQuotation {
108 | return this.splice(pos, 0, ...terms);
109 | }
110 | prepend(...terms: CatExpr[]): CatQuotation {
111 | return this.insert(0, ...terms);
112 | }
113 | append(...terms: CatExpr[]): CatQuotation {
114 | return this.insert(this.terms.length, ...terms)
115 | }
116 | }
117 |
118 | export class CatConstant extends CatExpr {
119 | constructor(public readonly value:T) {
120 | super();
121 | }
122 | public toString() : string {
123 | return this.value.toString();
124 | }
125 | }
126 |
127 | export class CatInstruction extends CatExpr {
128 | constructor(public readonly name:string) {
129 | super();
130 | }
131 | public toString() : string {
132 | return this.name;
133 | }
134 | }
--------------------------------------------------------------------------------
/combinatory-logic.ts:
--------------------------------------------------------------------------------
1 | // http://www.angelfire.com/tx4/cus/combinator/birds.html
2 | // https://www.amazon.com/exec/obidos/tg/detail/-/0394534913/104-1615637-3868724
3 | // https://en.wikipedia.org/wiki/Iota_and_Jot
4 | // https://web.archive.org/web/20160507165333/http://semarch.linguistics.fas.nyu.edu/barker/Iota
5 | // https://en.wikipedia.org/wiki/Combinatory_logic
6 | // https://en.wikipedia.org/wiki/B,_C,_K,_W_system
7 |
8 | import * as lc from './lambda-calculus';
9 | import { parseRedex, Redex, RedexName, RedexAbstraction, RedexApplication, isFreeVariableIn } from './lambda-calculus';
10 |
11 | export const combinators = {
12 | I : "\\x.(x)",
13 | K : "\\x.\\y.(x)",
14 | S : "\\x.\\y.\\z.(x z (y z))",
15 | B : "\\x.\\y.\\z.(x (y z))",
16 | C : "\\x.\\y.\\z.(x y z)",
17 | W : "\\x.\\y.(x y y)",
18 | D : "\\a.\\b.\\c.\\d.(a b (c d))",
19 | E : "\\a.\\b.\\c.\\d.\\e.(a b (c d e))",
20 | F : "\\a.\\b.\\c.(c b a)",
21 | G : "\\a.\\b.\\c.\\d.(a d (b c))",
22 | H : "\\a.\\b.\\c.(a b c b)",
23 | J : "\\a.\\b.\\c.\\d.(a b (a d c))",
24 | L : "\\a.\\b.(a (b b))",
25 | M : "\\x.(x x)",
26 | M2 : "\\a.\\b.(a b (a b))",
27 | O : "\\a.\\b.(b (a b))",
28 | Q : "\\a.\\b.\\c.(b (a c))",
29 | R : "\\a.\\b.\\c.(b (c a))",
30 | T : "\\a.\\b.(b a)",
31 | V : "\\a.\\b.\\c.(c a b)",
32 | Y : "\\f.(\\x.(f (x x) \\x.(f (x x))))",
33 | SUCC : "\\n.\\f.\\x.(f (n f x))",
34 | PRED : "\\n.\\f.\\x.(n (\\g.\\h.h (g f)) (\\u.x) (\\t.t))",
35 | PLUS : "\\m.\\n.\\f.\\x.(m f (n f x))",
36 | MUL : "\\m.\\n.\\f.(m (n f))",
37 | ZERO : "\\f.\\x.(x)",
38 | ONE : "\\f.\\x.(f x)",
39 | TWO : "\\f.\\x.(f (f x))",
40 | THREE : "\\f.\\x.(f (f (f x)))",
41 | TRUE : "\\x.\\y.(x)",
42 | FALSE : "\\x.\\y.(y)",
43 | PAIR : "\\x.\\y.\\f.(f x y)",
44 | FIRST : "\\p.(p \\x.\\y.(x))",
45 | SECOND : "\\p.(p \\x.\\y.(y))",
46 | NIL : "\\a.\\x.\\y.(x)",
47 | NULL : "\\p.(p (\\a.\\b.\\x.\\y.y))",
48 | IOTA : "\\f.((f \\x.\\y.\\z.(x z (y z))) \\x.\\y.(x))",
49 | };
50 |
51 | export const combinatorRedexes = {};
52 | for (var k in combinators)
53 | combinatorRedexes[k] = parseRedex(combinators[k]);
54 |
55 | export function parseCombinator(s: string) : Redex {
56 | // Inject white-spaces
57 | s = s.split('').join(' ');
58 | return parseRedex(s);
59 | }
60 |
61 | export function combinatorToLambdaCalculus(x: Redex) : Redex {
62 | if (x instanceof RedexName && x.name in combinatorRedexes)
63 | return combinatorRedexes[x.name];
64 | else if (x instanceof RedexAbstraction)
65 | return new RedexAbstraction(x.param, combinatorToLambdaCalculus(x.body))
66 | else if (x instanceof RedexApplication)
67 | return new RedexApplication(combinatorToLambdaCalculus(x.args), combinatorToLambdaCalculus(x.func));
68 | else
69 | return x;
70 | }
71 |
72 | // https://en.wikipedia.org/wiki/Combinatory_logic#Completeness_of_the_S-K_basis
73 | export function abstractionElimination(x: Redex): Redex {
74 | // T[(E₁ E₂)] => (T[E₁] T[E₂])
75 | if (x instanceof RedexApplication)
76 | {
77 | return new RedexApplication(abstractionElimination(x.func), abstractionElimination(x.args));
78 | }
79 | else if (x instanceof RedexAbstraction)
80 | {
81 | if (isFreeVariableIn(x.param, x.body))
82 | {
83 | // T[λx.x] => I
84 | if (x.body instanceof RedexName && x.body.name == x.param)
85 | {
86 | return new RedexName('I');
87 | }
88 | //T[λx.λy.E] => T[λx.T[λy.E]] (if x occurs free in E)
89 | else if (x.body instanceof RedexAbstraction)
90 | {
91 | return abstractionElimination(
92 | new RedexAbstraction(
93 | x.param,
94 | abstractionElimination(x.body)));
95 | }
96 | // T[λx.(E₁ E₂)] => (S T[λx.E₁] T[λx.E₂]) (if x occurs free in E₁ or E₂)
97 | else if (x.body instanceof RedexApplication)
98 | {
99 | return new RedexApplication(
100 | new RedexApplication(
101 | new RedexName('S'),
102 | abstractionElimination(new RedexAbstraction(x.param, x.body.func))),
103 | abstractionElimination(new RedexAbstraction(x.param, x.body.args)));
104 | }
105 | }
106 | else
107 | {
108 | // T[λx.E] => (K T[E]) (if x does not occur free in E)
109 | return new RedexApplication(
110 | new RedexName('K'), abstractionElimination(x.body));
111 | }
112 | }
113 | else {
114 | // T[x] => x
115 | return x;
116 | }
117 | }
118 |
119 | // TODO:
--------------------------------------------------------------------------------
/notes.txt:
--------------------------------------------------------------------------------
1 | Application
2 | ===========
3 |
4 | 1. I can't use the same "unifier" at the top-level. When evaluating a child expression, yes, I reuse it. However: when
5 | evaluating from left to right, the symbols have to be thrown out.
6 |
7 | t(u)
8 |
9 | Never affects the type of t. Right?
10 |
11 | However, it does tell us something interesting about how it is used, so we do start to learn a few things about it.
12 |
13 | We do know it is a function.
14 |
15 | We know that that function has to accept u as inputs. But it might not accept ONLY U as inputs.
16 |
17 | My question is, does modeling this as function composition change anything?
18 |
19 | t(u) => [u] [t] apply
20 |
21 | compose([u], [t], apply)?
22 |
23 | Let's try!
24 |
25 | //===
26 |
27 | So the revelation is that I should keep "application" general. Applying f(t) does not mean that f only works on t.
28 |
29 | I'm going to have to test that more in Cat.
30 |
31 | At the same time, the result is very important. And the result can depend on the particular usage of the function.
32 |
33 | //==
34 |
35 | The tricky part is using the right unifier. One unifier in place A makes sense, and another in place B, makes sense.
36 |
37 | //==
38 |
39 | f g compose apply
40 |
41 | x=>g=>f=>x'
42 |
43 | \x.f(g(x)) == x f.g apply
44 |
45 | //==
46 |
47 | Rewrite without terms? Rewrite without free variables?
48 |
49 | //==
50 |
51 | 1. Lambda-lifting
52 | 2. Alpha-renaming
53 |
54 | var _this = this;
55 | this.recExpr = m.delay(() => _this.expr);
56 | this.var = m.identifier.ast;
57 | this.number = m.digits.ast;
58 | this.boolean = m.keyword("true").or(m.keyword("false")).ast;
59 | this.abstraction = m.guardedSeq("\\", this.var, ".").then(this.recExpr).ast;
60 | this.parenExpr = m.guardedSeq("(", this.recExpr, ")").ast;
61 | this.binaryOperator = m.choice('<=','>=','==','!=','<','>','+','-','*','/','%').ast;
62 | this.unaryOperator = m.choice('+','-','!').ast;
63 | this.unaryExpr = m.guardedSeq(this.unaryOperator, m.ws, this.recExpr).ast;
64 | this.binaryExpr = m.seq(this.recExpr, m.ws, this.binaryOperator, m.ws, this.recExpr).ast;
65 | this.expr = m.choice(this.parenExpr, this.abstraction, this.unaryExpr, this.boolean, this.number, this.var).then(m.ws).oneOrMore.ast;
66 |
67 |
68 | // Unused.
69 | export type Operator = '+'|'-'|'*'|'/'|'%'|'<'|'>'|'<='|'>='|'=='|'!='|'!';
70 |
71 | export class ArithmeticExpr extends Redex {
72 | constructor(
73 | public readonly operator:string,
74 | public readonly args:Redex[])
75 | {
76 | super();
77 | if (args.length < 1) throw new Error("At least one argument is required");
78 | if (args.length > 2) throw new Error("At most two arguments are supported");
79 | }
80 |
81 | toString() : string {
82 | if (this.args.length == 2) return "(" + this.args[0] + " " + this.operator + " " + this.args[1] + ")";
83 | if (this.args.length == 1) return "(" + this.operator + " " + this.args[0] + ")";
84 | throw new Error("Only one or two operators are supported");
85 | }
86 |
87 | clone(lookup:IStringLookup={}) : ArithmeticExpr {
88 | return new ArithmeticExpr(this.operator, this.args.map(a => a.clone(lookup)))
89 | }
90 | }
91 |
92 | /*
93 | # Lambda Calculus Implementation
94 |
95 | This file is an implementation of a parser, evaluator, and a couple of core algorithms for the
96 | Lambda calculus. Yes, dear reader, you may say this has already been done and it is called "Lisp".
97 |
98 | * http://www.users.waitrose.com/~hindley/SomePapers_PDFs/2006CarHin,HistlamRp.pdf
99 | * https://en.wikipedia.org/wiki/Lambda_calculus
100 | * https://en.wikipedia.org/wiki/Free_variables_and_bound_variables
101 | * https://en.wikipedia.org/wiki/Lambda_lifting
102 | */
103 |
104 | Building a Programming Language
105 |
106 | 1. Example programming languages
107 | 1. Language categories
108 | 1. Lambda calculus
109 | 1. Type theory
110 | 1. Stack-Based languages
111 | 1. Combinatory logic
112 | 1. Turing machines
113 | 1. Van Neuman Architecture
114 | 1. Lisp
115 | 1. Forth
116 |
117 | //==
118 |
119 | # Implementing versus Embedding Concatenative Languages
120 |
121 | We implement a strongly typed concatenative language, Cat, which supports type-inference, but cannot be fully
122 | embedded in a programming language based on the HM type system (like Haskell) while preserving the semantics and
123 | type-inference.
124 |
125 | # Overview
126 |
127 | Strongly typed languages based on the HM type system like Haskell and ML, do not fully support the
128 | embedding of strongly typed concatenative languages like Cat. The problem arises when trying to derive
129 | a type when duplicating a polymorphic functions on the stack.
130 |
131 | Paper structure. What we show. What we don't show. Why we think it is the way it is? Where are the proofs?
132 |
133 | ## Cat Evaluator
134 |
135 | ## Cat Primitive Operations
136 |
137 | ## The Cat Type System
138 |
139 | ## The Cat Type Inference Algorithm
140 |
141 | ## The type of `quote dup`
142 |
143 | ## Converting from the Lambda Calculus to Cat
144 |
145 | ## Converting from Cat to the Lambda Calculus
146 |
147 | ## Future Work
148 |
149 | ## Open Questions
150 |
151 | //==
152 |
153 | # Converting from the Lambda Calculus to a Concatenative Language
154 |
155 | We present an algorithm for converting from the lambda calculus to a point-free form.
156 |
157 | //==
158 |
159 | touch => dup pop
160 |
161 | From Lambda calculus to Cat
162 | 1. \x.[] => [pop]
163 | 1. \x.[x] => [touch]
164 | 1. \x.[x x] => [dup]
165 | 1. \x.[\y.[x]
166 | 1. x y => apply
167 |
168 | What is interesting is that Cat is more explicit about every action that happens to the environment
169 | than the Lambda calculus. These are the advantages that Henry Baker talks about when he describes
170 | "Linear Lisp".
171 |
172 | 1. Elimination of variables
173 | 1. Replication of variables
174 | 1. Changing order of variables
175 | 1. Application of functions
176 |
177 | Let's consider a "Lambda Cat".
178 |
179 | //==
180 |
181 | // \a T => [T] dip \a
182 |
183 | \a \b T0 T1 @b T2 @a T3 ... TN
184 | [\b T0 T1 @b T2] dip \a @a T3
185 |
186 | NO!
187 |
188 | [\b T0 T1 @b T2] swap [T3 ... TN] papply [apply] dip apply
189 |
190 | or
191 |
192 | [\b T0 T1 @b T2] swap [T3 ... TN] papply compose apply
193 |
194 | This becomes interesting, because we are moving the variable exactly to where it is being used.
195 |
196 | Otherwise we get all sorts of weirdness where every variable is passed as an argument to every
197 | lambda that is invoked.
198 |
--------------------------------------------------------------------------------
/lambda-calculus.ts:
--------------------------------------------------------------------------------
1 | // A library of algorithms and data-structures for working with the simply untyped call-by-value Lambda calculus
2 | // extended with integer constants written in TypeScript.
3 | //
4 | // Copyright 2017 by Christopher Diggins
5 | // Licensed under the MIT License
6 |
7 | import { Myna as m } from "myna-parser";
8 |
9 | // A Myna grammar for parsing the lambda calculus with integers and boolean constants
10 | // In this grammar abstraction has a higher precedence than application. This means:
11 | // \a.a \b.b == \a.(a (\b.(b)))
12 | var grammar = new function()
13 | {
14 | var _this = this;
15 | this.recExpr = m.delay(() => _this.expr);
16 | this.var = m.identifier.ast;
17 | this.number = m.digits.ast;
18 | this.boolean = m.keyword("true").or(m.keyword("false")).ast;
19 | this.abstraction = m.guardedSeq("\\", this.var, ".").then(this.recExpr).ast;
20 | this.parenExpr = m.guardedSeq("(", this.recExpr, ")").ast;
21 | this.expr = m.choice(this.parenExpr, this.abstraction, this.boolean, this.number, this.var).then(m.ws).oneOrMore.ast;
22 | }
23 | m.registerGrammar('lambda', grammar, grammar.expr);
24 |
25 | // Parser for a lambda expression s
26 | const lcParser = m.parsers['lambda'];
27 |
28 | //=======================================================================
29 | // Helper classes
30 |
31 | export interface StringLookup {
32 | [name:string] : string;
33 | }
34 |
35 | //========================================================================
36 | // Classes representing different types of Lambda expressions
37 |
38 | // A redex stands for reducible expression. It is also called a term.
39 | // It is any valid expression in our lambda calculus.
40 | export class Redex {
41 | clone(lookup: StringLookup) : Redex {
42 | throw new Error("clone needs to be implemented in an derived class");
43 | }
44 | }
45 |
46 | // Represents constants like integers or booleans.
47 | export class RedexValue extends Redex {
48 | constructor(
49 | public readonly value:T)
50 | {
51 | super();
52 | }
53 |
54 | toString() : string {
55 | return this.value.toString();
56 | }
57 |
58 | clone(lookup:StringLookup) : RedexValue {
59 | return new RedexValue(this.value);
60 | }
61 | }
62 |
63 | // Represents names which might be free or bound variables
64 | export class RedexName extends Redex {
65 | constructor(
66 | public readonly name:string)
67 | {
68 | super();
69 | }
70 |
71 | toString() : string {
72 | return this.name.toString();
73 | }
74 |
75 | clone(lookup:StringLookup) : RedexName {
76 | return new RedexName(rename(this.name, lookup));
77 | }
78 | }
79 |
80 | // Represents the application of a function value to an argument value.
81 | // In the lambda calculus this can be written as `f(x)` or simply `f x`
82 | export class RedexApplication extends Redex {
83 | constructor(
84 | public readonly func:Redex,
85 | public readonly args:Redex)
86 | {
87 | super();
88 | }
89 |
90 | toString() : string {
91 | return this.func + "(" + this.args + ")";
92 | }
93 |
94 | clone(lookup:StringLookup={}) : RedexApplication {
95 | return new RedexApplication(this.func.clone(lookup), this.args.clone(lookup));
96 | }
97 | }
98 |
99 | // Represents a Lambda abstraction. Also called an anonymous function.
100 | // In the Lambda calculus all functions take one argument and return one value.
101 | export class RedexAbstraction extends Redex {
102 | constructor(
103 | public readonly param:string,
104 | public readonly body:Redex
105 | )
106 | {
107 | super();
108 | }
109 |
110 | toString() : string {
111 | return "\\" + this.param + "." + "(" + this.body + ")";
112 | }
113 |
114 | clone(lookup:StringLookup={}) : RedexAbstraction {
115 | return new RedexAbstraction(rename(this.param, lookup), this.body.clone(lookup));
116 | }
117 | }
118 |
119 | //===============================================================================
120 | // Parsing functions
121 |
122 | // Converts a string to a lambda expression (aka a reducible expression)
123 | export function parseRedex(s:string) : Redex {
124 | var ast = lcParser(s);
125 | if (ast.end != s.length)
126 | throw new Error("Whole input was not parsed");
127 | return astToRedex(ast);
128 | }
129 |
130 | // Converts an abstract syntax tree representation to an expression
131 | function astToRedex(ast:m.AstNode) : Redex {
132 | switch (ast.rule.name)
133 | {
134 | case "abstraction":
135 | return new RedexAbstraction(ast.children[0].allText, astToRedex(ast.children[1]));
136 | case "parenExpr":
137 | return astToRedex(ast.children[0]);
138 | case "var":
139 | return new RedexName(ast.allText);
140 | case "boolean":
141 | return new RedexValue(ast.allText.toLowerCase() == 'true');
142 | case "number":
143 | return new RedexValue(parseInt(ast.allText));
144 | case "expr":
145 | {
146 | var r = astToRedex(ast.children[0]);
147 | for (var i=1; i < ast.children.length; ++i) {
148 | var cur = astToRedex(ast.children[i])
149 | r = new RedexApplication(r, cur);
150 | }
151 | return r;
152 | }
153 | default:
154 | throw new Error("Unrecognized ast rule " + ast.rule);
155 | }
156 | }
157 |
158 | //===============================================================================
159 | // Helper functions
160 |
161 | // Returns the expression and all of its sub-expressions as an array
162 | export function getSubExpressions(exp:Redex, r:Redex[] = []) : Redex[] {
163 | r.push(exp);
164 | if (exp instanceof RedexApplication) {
165 | getSubExpressions(exp.func, r);
166 | getSubExpressions(exp.args, r);
167 | }
168 | else if (exp instanceof RedexAbstraction) {
169 | getSubExpressions(exp.body, r);
170 | }
171 | return r;
172 | }
173 |
174 | // Converts an array of strings to a string->string lookup table
175 | export function stringsToObject(strings:string[]) : StringLookup {
176 | var r:StringLookup = {};
177 | for (var s of strings)
178 | r[s] = s;
179 | return r;
180 | }
181 |
182 | // Converts the keys of an object ot an array of sorted strings
183 | export function keysAsStrings(object:any) : string[] {
184 | return Object.keys(object).sort();
185 | }
186 |
187 | // Returns a string corresponding value in the lookup table, if present.
188 | export function rename(name:string, lookup:StringLookup) : string {
189 | return name in lookup ? lookup[name] : name;
190 | }
191 |
192 | // Returns a reversed copy of a string
193 | export function reverse(xs:T[]) : T[] {
194 | return xs.slice(0).reverse();
195 | }
196 |
197 | // Returns all variables that are not parameters
198 | export function freeVariables(exp:Redex, r:StringLookup={}, vars:StringLookup={}) : StringLookup {
199 | if (exp instanceof RedexAbstraction) {
200 | return freeVariables(exp.body, r, { ...vars, ...stringsToObject([exp.param]) });
201 | }
202 | else if (exp instanceof RedexApplication) {
203 | freeVariables(exp.func, r, vars);
204 | return freeVariables(exp.args, r, vars);
205 | }
206 | else if (exp instanceof RedexName) {
207 | if (!(exp.name in vars))
208 | r[exp.name] = exp.name;
209 | }
210 | return r;
211 | }
212 |
213 | // Returns true if the named variable occurs in the expression
214 | export function isFreeVariableIn(v:string, exp:Redex) : boolean {
215 | var free = freeVariables(exp);
216 | return v in free;
217 | }
218 |
219 | // Converts an expression by lifting a free variable out into an argument of an abstraction
220 | // and applying that abstraction to a variable with the name. (i) => (\i.(i))(i)
221 | // This is a- step of the lambda lift operation
222 | export function lambdaLiftVar(v:string, exp:Redex) : Redex {
223 | return new RedexApplication(new RedexAbstraction(v, exp), new RedexName(v));
224 | }
225 |
226 | // Removes all free variables from an expression
227 | export function lambdaLift(exp:Redex) : Redex {
228 | if (exp instanceof RedexAbstraction) {
229 | var r:Redex = new RedexAbstraction(exp.param, lambdaLift(exp.body));
230 | const freeVars:string[] = keysAsStrings(freeVariables(r));
231 | for (var v of freeVars)
232 | r = lambdaLiftVar(v, r);
233 | return r;
234 | }
235 | else if (exp instanceof RedexApplication) {
236 | return new RedexApplication(lambdaLift(exp.func), lambdaLift(exp.args));
237 | }
238 | else {
239 | return exp;
240 | }
241 | }
242 |
243 | // Returns true is the redex is a variable and if a name is provided, matches the ame
244 | export function isVar(exp:Redex, name:string = null) : boolean {
245 | if (exp instanceof RedexName) {
246 | if (name == null)
247 | return true;
248 | return exp.name == name;
249 | }
250 | return false;
251 | }
252 |
253 | // http://www.lambda-bound.com/book/lambdacalc/node21.html
254 | // https://en.wiktionary.org/wiki/eta_conversion
255 | export function etaConversion(exp:Redex) : Redex {
256 | if (exp instanceof RedexAbstraction) {
257 | var body = exp.body;
258 | if (body instanceof RedexApplication)
259 | if (isVar(body.args, exp.param))
260 | if (!isFreeVariableIn(exp.param, body.func))
261 | return body;
262 | }
263 | return exp;
264 | }
--------------------------------------------------------------------------------
/cat-lambda.ts:
--------------------------------------------------------------------------------
1 | // Lambda Cat is a variation of cat that supports parameters and variables
2 | // Conversion Rules from Lambda Cat to Pure Cat
3 | //
4 | // 0) Alpha rename Lambda Cat to assure that all parameters have a unique name
5 | // 1) every variable \x where there is no occurence of @x in scope, is replaced by pop.
6 | // 2) every variable \x that has two or more references to x in scope, replace "\x" with "dupN \x1 \x2 ... \xN".
7 | // Rename references to \x in order to @x1 @x2 ... @xN
8 |
9 | import { CatExpr, CatQuotation, CatInstruction, CatParam, CatVar, CatCapture, CatConstant } from './cat-parser';
10 |
11 | export type Usage = {
12 | name:string;
13 | depth:number;
14 | }
15 |
16 | export type Declaration = {
17 | name:string;
18 | depth:number;
19 | }
20 |
21 | export interface IUsages {
22 | [name:string] : Usage[];
23 | }
24 |
25 | export interface INameLookup {
26 | [name:string] : string;
27 | }
28 |
29 | export interface NameCount {
30 | [name:string] : number;
31 | }
32 |
33 | export function getVarUsages(xs:CatExpr[], usages:IUsages={}, depth:number=0) : IUsages {
34 | xs.forEach(x => getVarUsagesSingle(x, usages, depth));
35 | return usages;
36 | }
37 |
38 |
39 | export function getVarUsagesSingle(x:CatExpr, usages:IUsages={}, depth:number=0) : IUsages {
40 | if (x instanceof CatParam) {
41 | if (x.name in usages)
42 | throw new Error("Parameter name used more than once");
43 | usages[x.name] = [];
44 | }
45 | else if (x instanceof CatVar) {
46 | if (!(x.name in usages))
47 | throw new Error("Variable not associated with parameter")
48 | usages[x.name].push({name:x.name, depth});
49 | }
50 | else if (x instanceof CatQuotation) {
51 | getVarUsages(x.terms, usages, depth+1);
52 | }
53 | return usages;
54 | }
55 |
56 | export function areVariablesSingleUse(xs: CatExpr[]): boolean {
57 | var names: NameCount = {};
58 | for (var expr of xs) {
59 | for (var x of leafExprs(expr)) {
60 | if (x instanceof CatParam) {
61 | if (x.name in names)
62 | throw new Error("Parameter already used")
63 | else
64 | names[x.name] = 0;
65 | }
66 | else if (x instanceof CatVar) {
67 | if (x.name in names)
68 | names[x.name]++;
69 | else
70 | throw new Error("Variable not accompanied by a parameter")
71 | }
72 | }
73 | }
74 | for (var k in names)
75 | if (names[k] != 1)
76 | return false;
77 | return true;
78 | }
79 |
80 | // We assume all variables are already uniquely named.
81 | // Given a usages analysis, this will run through the variables and assign names to each instance,
82 | // and increase the number of parameters.
83 | export function makeVarsSingleUse(expr: CatExpr[], usages:IUsages, renaming:NameCount={}) : CatExpr[] {
84 | let r:CatExpr[] = [];
85 | for (let i=0; i < expr.length; ++i) {
86 | let t = expr[i];
87 | if (t instanceof CatParam) {
88 | if (!(t.name in usages))
89 | throw new Error("Could not find cat variable " + t.name);
90 | let n = usages[t.name].length;
91 | if (n == 0) {
92 | r.push(new CatInstruction("pop"));
93 | }
94 | for (let j=0; j < n; ++j) {
95 | if (j < n-1) {
96 | r.push("dup");
97 | }
98 | r.push(new CatParam(t.name + '#' + j));
99 | }
100 | renaming[t.name] = 0;
101 | }
102 | else if (t instanceof CatVar) {
103 | var tmp = t.name + '#' + (renaming[t.name]++).toString();
104 | r.push(new CatVar(tmp));
105 | }
106 | else if (t instanceof CatQuotation) {
107 | r.push(new CatQuotation(makeVarsSingleUse(t.terms, usages, renaming)));
108 | }
109 | else {
110 | r.push(t);
111 | }
112 | }
113 | return r;
114 | }
115 |
116 | export interface StringLookup {
117 | [name: string] : string;
118 | }
119 |
120 | export interface NameGenerator {
121 | (name: string) : string;
122 | }
123 |
124 | export function makeNameGenerator() {
125 | var n = 0;
126 | return (name: string) => name + '$' + n++;
127 | }
128 |
129 | export function quotationContainsVar(q: CatQuotation, x: string) {
130 | for (var i=0; i < q.terms.length; ++i) {
131 | var t = q.terms[i];
132 | if (t instanceof CatVar) {
133 | if (t.name === x)
134 | return true;
135 | }
136 | else if (t instanceof CatQuotation) {
137 | if (quotationContainsVar(t, x))
138 | return true;
139 | }
140 | }
141 | return false;
142 | }
143 |
144 | export function leafExprs(expr: CatExpr, r: CatExpr[] = []): CatExpr[] {
145 | if (expr instanceof CatQuotation) {
146 | expr.terms.forEach(t => leafExprs(t, r));
147 | }
148 | else {
149 | r.push(expr);
150 | }
151 | return r;
152 | }
153 |
154 | export function areVariablesUniquelyNamed(expr: CatQuotation): boolean {
155 | var names: { [name:string] : string } = {};
156 | for (var x of leafExprs(expr)) {
157 | if (x instanceof CatParam) {
158 | if (x.name in names)
159 | return false;
160 | else
161 | names[x.name] = x.name;
162 | }
163 | }
164 | return true;
165 | }
166 |
167 | export function areVariablesRemoved(x: CatExpr): boolean {
168 | if (x instanceof CatVar)
169 | return false;
170 | else if (x instanceof CatParam)
171 | return false;
172 | else if (x instanceof CatQuotation)
173 | return x.terms.every(areVariablesRemoved);
174 | return true;
175 | }
176 |
177 | // Uniquely names each variable.
178 | export function uniquelyNameVarsSingle(t:CatExpr, nameGen:NameGenerator = makeNameGenerator(), lookup:StringLookup={}) : CatExpr {
179 | if (t instanceof CatParam) {
180 | lookup[t.name] = nameGen(t.name);
181 | return new CatParam(lookup[t.name]);
182 | }
183 | else if (t instanceof CatVar) {
184 | if (!(t.name in lookup))
185 | throw new Error("Variable could not be found " + t.name);
186 | return new CatVar(lookup[t.name]);
187 | }
188 | else if (t instanceof CatQuotation) {
189 | return new CatQuotation(uniquelyNameVars(t.terms, nameGen, {...lookup}));
190 | }
191 | else {
192 | return t;
193 | }
194 | }
195 |
196 | export function uniquelyNameVars(xs: CatExpr[], nameGen: NameGenerator = makeNameGenerator(), lookup:StringLookup={}) : CatExpr[] {
197 | return xs.map(t => uniquelyNameVarsSingle(t, nameGen, lookup));
198 | }
199 |
200 | export function isLambdaExpr(x: CatExpr) {
201 | return x instanceof CatParam
202 | || x instanceof CatVar
203 | || (x instanceof CatQuotation && x.terms.some(isLambdaExpr));
204 | }
205 |
206 | // First uniquely names variables and assures that all variables are single usage.
207 | // The last \x will be immediately followed by an expression.
208 | // 1. \x x => id
209 | // 2. \x [T] be a quotation containing x => [\x T] papply
210 | // 3. \x T where T neither is nor contains an occurence of @x => [T] dip \x
211 | export function removeVars(xs: CatExpr[]): CatExpr[] {
212 | // If none of the child expressions are a lambda-expression, then we are done
213 | if (!(xs.some(isLambdaExpr)))
214 | return xs;
215 |
216 | // Make a copy to be safe
217 | var r = [...xs];
218 |
219 | // Uniquely name variables
220 | r = uniquelyNameVars(r);
221 |
222 | // Compute all of the usages
223 | var usages = getVarUsages(r);
224 |
225 | // Make sure each variable is used once
226 | r = makeVarsSingleUse(r, usages);
227 |
228 | // Double check it worked
229 | if (!areVariablesSingleUse(r))
230 | throw new Error('Variables are not only used once');
231 |
232 | // Find the last \param
233 | var i = r.length;
234 | while (i-- > 0)
235 | {
236 | var t = r[i];
237 | if (t instanceof CatParam) {
238 | if (i == r.length-1)
239 | throw new Error("Parameter should never occur at end");
240 |
241 | var next = r[i+1];
242 |
243 | // \a @a => noop
244 | if (next instanceof CatVar && next.name === t.name) {
245 | r.splice(i, 2);
246 | }
247 | // \a [.. @a ...] => [\a ... @a ...] papply
248 | // Note: this might not happen if we do lambda-lifting first.
249 | else if (next instanceof CatQuotation && quotationContainsVar(next, t.name)) {
250 | r.splice(i, 2, next.prepend(t), new CatInstruction('papply'));
251 | }
252 | // \a T1 T2 => [T1] swap [\a T2] papply compose apply
253 | else {
254 | // This keeps the generate algorithms simple
255 | var before = [next];
256 | var j = i + 2;
257 | while (j < r.length) {
258 | var tmp = r[j];
259 | if (tmp instanceof CatVar && tmp.name === t.name)
260 | break;
261 | if (tmp instanceof CatQuotation && quotationContainsVar(tmp, t.name))
262 | break;
263 | before.push(tmp);
264 | j++;
265 | }
266 | var after = r.slice(j);
267 | after.splice(0, 0, t);
268 |
269 | r.splice(i, r.length - i, new CatQuotation(before), new CatInstruction('swap'),
270 | new CatQuotation(after), new CatInstruction("papply"), new CatInstruction("compose"),
271 | new CatInstruction("apply"));
272 | }
273 | /*
274 | // \a T => [T] dip \a
275 | else {
276 | // Figure out exactly how many terms we can put in the dip.
277 | // This keeps the generate algorithms simple
278 | var skip = [next];
279 | var j = i + 2;
280 | while (j < r.length) {
281 | var tmp = r[j];
282 | if (tmp instanceof CatVar && tmp.name === t.name)
283 | break;
284 | if (tmp instanceof CatQuotation && quotationContainsVar(tmp, t.name))
285 | break;
286 | skip.push(tmp);
287 | j++;
288 | }
289 | r.splice(i, skip.length + 1, new CatQuotation(skip), new CatInstruction('dip'), t);
290 | }
291 | */
292 | i = r.length;
293 | }
294 | }
295 |
296 | // Recurisvely remove vars
297 | for (var i=0; i < r.length; ++i)
298 | {
299 | var t = r[i];
300 | if (t instanceof CatQuotation)
301 | r[i] = new CatQuotation(removeVars(t.terms));
302 | }
303 |
304 | // Create a new quotation
305 | if (r.some(x => !areVariablesRemoved(x)))
306 | throw new Error("Failed to remove variables");
307 |
308 | if (!areVariablesSingleUse(r))
309 | throw new Error("Variables are supposed to be single use");
310 |
311 | return r;
312 | }
313 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Type Inference for Polymorphic Types
2 |
3 | This is a TypeScript implementation of a type-inference algorithm for polymorphic types.
4 | It is used in the:
5 | * [Heron programming language](https://github.com/cdiggins/heron-language)
6 | * [Cat programming language](https://github.com/cdiggins/cat-language)
7 |
8 | The type inference algorithm is not proven to support rank-N polymorphism in all cases (this is an open question),
9 | but it does infer polymorphic types in some cases where other languages using HM inference are known to fail.
10 |
11 | # Citation
12 |
13 | To formally cite this project please use:
14 |
15 | ```
16 | @software{cdiggins/type-inference,
17 | author = {Diggins, Christopher},
18 | title = {Type Inference for Polymorphic Types},
19 | url = {https://github.com/cdiggins/type-inference},
20 | year = {2017},
21 | }
22 | ```
23 |
24 | # Motivation
25 |
26 | Types for higher-order functions using generics can become more complex than the implementations of the functions.
27 | If the types of generic (polymorphic) functions can be entirely inferred, then it can make the language
28 | easier to learn, read, and teach.
29 |
30 | The following screen shot demonstrates the inferred types for a number of higher-order functions
31 | for the [Heron standard library](https://github.com/cdiggins/heron-language/blob/master/input/array.heron).
32 |
33 | 
34 |
35 | In data-flow visual languages it is not practical to expect a user to provide types, so type inference
36 | is a critical feature to achieve good performance. In 3ds Max the
37 | [Max Creation Graph - MCG](https://help.autodesk.com/view/3DSMAX/2017/ENU/?guid=GUID-608EC963-75ED-4F63-96B7-D8AE57E75959) feature
38 | is a visual data-flow programming language primarily used for creating procedural geometry and modifiers.
39 | MCG supports higher-order functions to represent control-flow, and performs type-inference on the fly.
40 |
41 | 
42 |
43 | Follow [this link for more information on higher order functions in MCG](https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=MAXDEV_MCG_arrays_and_functions_apply_and_bind_html).
44 |
45 | # History
46 |
47 | This algorithm came from earlier work done on developing a type system for functional stack-based languages which
48 | was documented in two technical reports.
49 |
50 | ```
51 | @article{article,
52 | author = {Diggins, Christopher},
53 | year = {2008},
54 | month = {05},
55 | pages = {},
56 | title = {Typing Functional Stack-Based Languages},
57 | volume = {33}
58 | }
59 | ```
60 |
61 | ```
62 | @article{article,
63 | author = {Diggins, Christopher},
64 | year = {2008},
65 | month = {01},
66 | pages = {},
67 | title = {Simple Type Inference for Higher-Order Stack-Oriented Languages}
68 | }
69 | ```
70 |
71 | # Source Code and Dependencies
72 |
73 | All of the source code is constained in a single TypeScript file [type_inference.ts](https://github.com/cdiggins/type-inference/blob/master/type_inference.ts). The tests are contained in the file [test.ts](https://github.com/cdiggins/type-inference/blob/master/test.ts). The tests have a dependency on the [Myna parser](https://github.com/cdiggins/myna-parser).
74 |
75 | # Implementation Overview
76 |
77 | The basic algorithm is as follows:
78 |
79 | * Step 1 (renaming): Uniquely name all type variables
80 | * Step 2 (forall inference): Infer the position of all forall quantifiers in type arrays
81 | * Step 3 (unify constraint): Given a constraint between two types find a unifier for each variable.
82 | * Step 4 (substitution): Given a type signature compute a new signature by subsituting all type variables in the given type signature with the appropriate unifying type.
83 | * Step 5 (forall inference): Infer the position of all forall quanitifer in the resulting type.
84 |
85 | During step 4 (substitution), if a polytype is substituted for a type variable more than once each subsequent instance is given a new set of names for the generic type parameters. This is key for the step 5 to be able to correctly assign the forall quantifiers to the right level of nesting.
86 |
87 | ## Implementation Details
88 |
89 | Types are represented by the `Type` class. There are three kinds of types derived from this class:
90 |
91 | 1. `TypeConstant` - Represents monotypes (simple non-plymorphic types)
92 | 2. `TypeVariable` - Represents a universally quantified type variable, also known as a generic type parameter.
93 | 3. `TypeArray` - A fixed-size collection of types that may or may not have universally quantified type parameters
94 |
95 | Other kinds of types, like functions and arrays, are encoded as special cases of type arrays.
96 |
97 | ## Recursive Types
98 |
99 | Recursive types are not supported but will be reported by the system when inferred. They are identified by a type pair with the special name `Rec` and the depth of the recursive relation as an integer. For example: `(Rec 0)`. Any type containing a `Rec` form should be considered illegal, and will not unify correctly, but the algorithm will nonetheless detect recurrence relations in type relations and report them as if they were a valid type.
100 |
101 | ## Type Constants
102 |
103 | A type constant is an atomic non-polymorphic type. It can be an identifier (e.g. `Num` or `Bool`), a number (e.g. `0` or `42`), or a symbol (i.e. `->` or `[]`). The symbols and numbers have no special meaning to the type inference algorithm and are just treated the same as ordinary type constants with identifiers. Type constants can be be used as unifiers associated with type variables and two type cosntants can't be unified together if they are different.
104 |
105 | ## Type Arrays
106 |
107 | A type array is an ordered collection of 0 or more types. It is written out as a sequence of type expressions seprated by whitespace and surrounded by parentheses. Some example are: `()`, `(Num Bool)`, `(a [])`, `(b -> c)`, `(a (b (c)))`.
108 |
109 | A type array may be monomorphic or polymorphic (i.e. have type parameters). Using symbolic type constants in a type array are used as a way of making them more readable and to help a language implementation associate different meaning to certain type arrays. For example: `(Num Num -> Bool)` can be used to represent the function type that takes two numbers and returns a boolean value, and the `((Num []) [])` can be used to represent an array of arrays of numbers.
110 |
111 | ## Polymorphic Types
112 |
113 | A polymorphic type (aka polytype, type scheme, generic type, or parametric type) is a type array that has one or more type parameters that are universally quantified. Type parameters are bound to universal quantifiers, represented as a `!` in the syntax.
114 |
115 | For example: `!a.(a [])`, `!a!b.(pair a b)`, `!a!b.((a []) Num (a -> b) -> (b []))`
116 |
117 | ## Type Variables
118 |
119 | Type variables are the free occurrences of a type parameters that can be replaced a valid type expression. A type variable must be contained polymorphic type that contains the type parameter.
120 |
121 | # Type Encodings
122 |
123 | In a practical setting it is important to encode many different kinds of types such as functions, tuples, structs, arrays, and more. These can all be described using type arrays with special type constants, and the type inference algorithm can deal with them seamlessly. In this section several different encodings are described.
124 |
125 | ## Encoding Function Types
126 |
127 | A function is encoded as a type array with 3 elements: the first element representing the arguments, the second element is the special type constant `->` and the third is a type that represents the function return types.
128 |
129 | For example the type `((Num Num) -> Bool)` represents a function that converts a type array of two `Num` types to a single `Bool` type.
130 |
131 | ## Encoding Type Lists
132 |
133 | A type list is encoded a pair of types (a type array of two elements) where the first element is the head of the list, and the second element is the rest of the list. Usually the type in the last position is a type variable which enables two type lists with different number of elements to be unified.
134 |
135 | ## Encoding Array Types
136 |
137 | An array type (not to be confused with an array of types) can be encoded as a type array of two elements: the type of elements in the followed by the special type constant `[]`.
138 |
139 | For example: `!T.((T []) -> Num)` encodes the type of a function that accepts convert
140 |
141 | # Top-Level Type Operators
142 |
143 | There are three top-level type operators provided:
144 |
145 | 1. Application - Given a function type, and the type of the arguments, returns the output type.
146 | 2. Composition - Given two functions types (f and g) returns a new type representing the composition of the two functions.
147 | 3. Quotation - Given a type x generates a row-polymorphic function that will take any type (a) and return the pair a and x.
148 |
149 | # Row Polymorphism
150 |
151 | Two sequences of types of different lengths can be unified if they are encoded as type lists with a type variable in the tail position.
152 |
153 | This is useful to express concepts like a function that accept all tuples that have at least one or two items, such as a `dup`, `pop`, or `swap` operation that works on stacks. The type then would be written as:
154 |
155 | ```
156 | pop : !a!S.((a S) -> S)))
157 | dup : !a!S.((a S) -> (a (a S)))
158 | swap : !a!b!S.(a (b S) -> (b (a S)))
159 | ```
160 |
161 | Note that lower case and upper case variables do not have different syntactic, if a variable is in a tail position it is implicitly row polymorphic because of the nature of unification of lists encoded as nested pairs.
162 |
163 | # How the Algorithm differs from Hindley Milner
164 |
165 | In Hindley Milner type inference all forall quantifiers are lifted to the top of a function. This is not immediately obvious when looking at common terms in the Lambda Calculus, but it becomes more obvious when we start looking at concatenative languages, i.e. stack based language with higher order functions, and consider the type of the term `quote dup`, or any term that makes a copy of a polymorphic function argument.
166 |
167 | Consider the following operators which are common in concatenative (i.e. functional stack based) languages:
168 |
169 | ```
170 | apply : !R.(!S.((S -> R) S) -> R)
171 | quote : !S!a.((a S) -> (!R.(R -> (a R)) S))
172 | compose : !A!C!S.(!B.((B -> C) ((A -> B) S)) -> ((A -> C) S))
173 | ```
174 |
175 | The `apply` function applies the function on the top of the stack to the rest of the stack. The `quote` function remove a value from the top of the top of the stack, and pushes a function on to the stack that will take any stack and push that value back onto the stack, and the compose function will take two functions off of the stack (say `f` on top followed by `g` underneath) and then push the composition of g with f (`g.f`) back onto the stack.
176 |
177 | Now consider the type inferred by the algorithm for the term `quote dup`. Notice that there are two independent forall quantifiers in the generated function. This is not possible for HM type inference.
178 |
179 | ```
180 | quote dup : !t0!t1.((t0 t1) -> (!t2.(t2 -> (t0 t2)) (!t3.(t3 -> (t0 t3)) t1)))
181 | ```
182 |
183 | When we infer the type for the term `quote dup apply` we get the following:
184 |
185 | ```
186 | quote dup apply : !t0!t1.((t0 t1) -> (t0 (t0 (t1)))
187 | ```
188 |
189 | Notice this is exactly the same type as for `dup` which is what we expect intuitively.
190 |
191 | ## Compared to Haskell
192 |
193 | The core stack terms can all be described easily in standard Haskell and with the type inferred, but the forall quantifiers are all implicitly lifted to the top level. Here is an example of an interactive session with Haskell
194 |
195 | ```
196 | GHCi, version 8.2.1: http://www.haskell.org/ghc/ :? for help
197 | Prelude> :set +t
198 | Prelude> dup (a, s) = (a, (a, s))
199 | dup :: (a, b) -> (a, (a, b))
200 | Prelude> pop (a, s) = s
201 | pop :: (a, b) -> b
202 | Prelude> swap (a, (b, s)) = (b, (a, s))
203 | swap :: (a1, (a2, b)) -> (a2, (a1, b))
204 | Prelude> quote (a, s) = (\r -> (a, r), s)
205 | quote :: (a, b1) -> (b2 -> (a, b2), b1)
206 | Prelude> apply (f, s) = f s
207 | apply :: (t1 -> t2, t1) -> t2
208 | Prelude> compose (f, (g, s)) = (g . f, s)
209 | compose :: (a -> b1, (b1 -> c, b2)) -> (a -> c, b2)
210 | ```
211 |
212 | So far everything looks the same as before, however the type of the expression `dup quote` in a concatenative language (`quote . dup` when expressed in a prefix applicative function language) the type inferred by Haskell is as follows:
213 |
214 | ```
215 | Prelude> quotedup = dup . quote
216 | quotedup :: (a, b) -> (b2 -> (a, b2), (b2 -> (a, b2), b))
217 | ```
218 |
219 | This type represents the two `quote` functions on the stack as being linked, they both require the same stack configuration `b2`. So when we are to try and compose this with apply, the type inference algorithm fails apart as expected with prenex polymorphism.
220 |
221 | ```
222 | Prelude> f = apply . quotedup
223 |
224 | :11:13: error:
225 | * Occurs check: cannot construct the infinite type:
226 | t1 ~ (t1 -> (a, t1), b)
227 | Expected type: (a, b) -> (t1 -> (a, t1), t1)
228 | Actual type: (a, b) -> (t1 -> (a, t1), (t1 -> (a, t1), b))
229 | * In the second argument of `(.)', namely `quotedup'
230 | In the expression: apply . quotedup
231 | In an equation for `f': f = apply . quotedup
232 | * Relevant bindings include
233 | f :: (a, b) -> (a, t1) (bound at :11:1)
234 | ```
235 |
236 | # Appendix: An Alternate Syntax of Type Signatures
237 |
238 | In the test system an alternate type syntax is proposed that allows the forall quantifiers to be ommitted and a syntactic analysis to identify type variables. Apostrophes are placed in front of identifiers to identify them as variables. Variables are assumed to be uniquely named, and the appropriate polytype is assumed.
239 |
240 | For example a function type `!a!b.((a b) -> (!c.(c -> (a c) b))` can be expressed as `(('a 'b) -> (('c -> 'a 'c) 'b))`.
241 |
242 | ## Inference of Location of For-all Quantifiers
243 |
244 | Given a type array containing type variables assumed to be uniqely named in potentially nested type arrays, the algorithm will infer which type parameters belong to which type arrays. In other words the precise location of the for-all quantifiers are determined. This process is done by assigning each type variables as a paraemter to the inner-most type array that contains all references to that type variable. This is done in the function `computeParameters()`.
245 |
246 | # For More Information
247 |
248 | * https://www.researchgate.net/publication/228985001_Typing_Functional_Stack-Based_Languages
249 | * https://prl.khoury.northeastern.edu/blog/static/stack-languages-annotated-bib.pdf
250 | * https://en.wikipedia.org/wiki/Parametric_polymorphism.
251 | * https://en.wikipedia.org/wiki/Hindley%E2%80%93Milner_type_system
252 | * https://en.wikipedia.org/wiki/Lambda_calculus
253 | * https://en.wikipedia.org/wiki/Typed_lambda_calculus
254 | * https://en.wikipedia.org/wiki/Unification_(computer_science)
255 | * https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)
256 | * https://www.cs.cornell.edu/courses/cs3110/2011sp/Lectures/lec26-type-inference/type-inference.htm
257 | * http://www.angelfire.com/tx4/cus/combinator/birds.html
258 | * https://github.com/leonidas/codeblog/blob/master/2012/2012-02-17-concatenative-haskell.md
259 |
260 |
--------------------------------------------------------------------------------
/test.ts:
--------------------------------------------------------------------------------
1 | // This is a set of tests for the type-inference algorithm applied to lambda Calculus and the Concatenative calculus.
2 | // Running these tests require installation of the Myna parsing module.
3 |
4 | // TODO: reject recursion and jump out.
5 | // TODO: check identities
6 | // TODO: from Cat to Lambda-cat (dup/pop/papply/dip)
7 | // TODO: rewriting rules
8 | // TODO: simplify the type presentation (real parseable version)
9 | // TODO: make type presentation look like TypeScript types
10 | // TODO: make type presentation look like Haskell types
11 |
12 | import { parseType } from "./type-parser";
13 | import { parseCat, CatExpr, CatQuotation } from "./cat-parser";
14 | import { catTypes, inferCatType } from "./cat-types";
15 | import { uniquelyNameVars, getVarUsages, removeVars, makeVarsSingleUse } from "./cat-lambda";
16 | import { parseRedex, freeVariables, etaConversion, Redex, lambdaLift } from "./lambda-calculus";
17 | import { abstractionElimination, combinators, combinatorToLambdaCalculus } from "./combinatory-logic";
18 | import { lambdaToPureCat, lambdaToCat } from "./lambda-calculus-to-cat";
19 | import { catStdOps } from "./cat-library";
20 | import { Type, normalizeVarNames, alphabetizeVarNames, isFunctionType, functionInput, TypeArray, TypeVariable, TypeConstant, functionOutput, typeToString } from "./type-system";
21 |
22 | //==========================================================================================
23 | // Testing helper functions
24 |
25 | export function runTest(testName:string, f:(TInput) => TOutput, input: TInput, output: TOutput=undefined, expectFail:boolean = false) {
26 | try {
27 | console.log("test " + testName);
28 | console.log(" input: " + input.toString());
29 | const result = f(input);
30 | console.log(" result: " + result.toString());
31 | const success = !result || (result && !expectFail || !result && expectFail);
32 | console.log(success ? " passed" : " FAILED");
33 | }
34 | catch (e) {
35 | if (expectFail) {
36 | console.log(" passed: expected fail, error caught: " + e.message);
37 | }
38 | else {
39 | console.log(" FAILED: error caught = " + e.message);
40 | }
41 | }
42 | }
43 |
44 | export function catTypeToString(t:Type) : string {
45 | if (t instanceof TypeVariable)
46 | return "'" + t.name;
47 | else if (t instanceof TypeArray)
48 | return "(" + t.types.map(catTypeToString).join(" ") + ")";
49 | else
50 | return t.toString();
51 | }
52 |
53 | export function compareTypes(t1:Type, t2:Type) {
54 | {
55 | var r1 = normalizeVarNames(t1).toString();
56 | var r2 = normalizeVarNames(t2).toString();
57 | if (r1 != r2) {
58 | throw new Error("Types are not the same when normalized: " + r1 + " and " + r2);
59 | }
60 | }
61 | {
62 | var r1 = alphabetizeVarNames(t1).toString();
63 | var r2 = alphabetizeVarNames(t2).toString();
64 | if (r1 != r2) {
65 | throw new Error("Types are not the same when alphabetized: " + r1 + " and " + r2);
66 | }
67 | }
68 | }
69 |
70 | // Some identities
71 | // NOTE: the intuitionistic logic part is interesting.
72 | // https://en.wikipedia.org/wiki/B,_C,_K,_W_system#Connection_to_intuitionistic_logic
73 | export const combinatorDefs = {
74 | "B" : "S (K S) K",
75 | "C" : "S (B B S)(K K)",
76 | "D" : "B B",
77 | "E" : "B (B B B)",
78 | "F" : "E T T E T",
79 | "G" : "B B C",
80 | "H" : "B W (B C)",
81 | "I" : "S K K",
82 | "J" : "B (B C) (W (B C (B (B B B))))",
83 | "L" : "C B M",
84 | "M" : "S I I",
85 | "O" : "S I",
86 | "Q" : "C B",
87 | "R" : "B B T",
88 | "T" : "C I",
89 | "U" : "L O",
90 | "V" : "B C T",
91 | "W" : "C (B M R)",
92 | "Y" : "S L L",
93 | "S" : "B (B W) (B B C)",
94 | "I2" : "W K",
95 |
96 | // TODO: test the arithmetic identities
97 | }
98 |
99 | const catTests = [
100 | // Primitive forms
101 | ["", "!t0.(t0 -> t0)"],
102 | // ["id", "!t0!t1.((t0 t1) -> (t0 t1))"],
103 | ["apply", "!t1.(!t0.((t0 -> t1) t0) -> t1)"],
104 | ["compose", "!t1!t2!t3.(!t0.((t0 -> t1) ((t2 -> t0) t3)) -> ((t2 -> t1) t3))"],
105 | ["quote", "!t0!t1.((t0 t1) -> (!t2.(t2 -> (t0 t2)) t1))"],
106 | ["dup", "!t0!t1.((t0 t1) -> (t0 (t0 t1)))"],
107 | ["swap", "!t0!t1!t2.((t0 (t1 t2)) -> (t1 (t0 t2)))"],
108 | ["pop", "!t1.(!t0.(t0 t1) -> t1)"],
109 |
110 | // Quotations of Primitives
111 | ["[]", "!t0.(t0 -> (!t1.(t1 -> t1) t0))"],
112 | //["[id]", "!t0.(t0 -> (!t1!t2.((t1 t2) -> (t1 t2)) t0))"],
113 | ["[apply]", "!t0.(t0 -> (!t2.(!t1.((t1 -> t2) t1) -> t2) t0))"],
114 | ["[pop]", "!t0.(t0 -> (!t2.(!t1.(t1 t2) -> t2) t0))"],
115 | ["[dup]", "!t0.(t0 -> (!t1!t2.((t1 t2) -> (t1 (t1 t2))) t0))"],
116 | ["[compose]", "!t0.(t0 -> (!t2!t3!t4.(!t1.((t1 -> t2) ((t3 -> t1) t4)) -> ((t3 -> t2) t4)) t0))"],
117 | ["[quote]", "!t0.(t0 -> (!t1!t2.((t1 t2) -> (!t3.(t3 -> (t1 t3)) t2)) t0))"],
118 | ["[swap]", "!t0.(t0 -> (!t1!t2!t3.((t1 (t2 t3)) -> (t2 (t1 t3))) t0))"],
119 |
120 | // Basic composition tests
121 | ["[[pop]]", "!t0.(t0 -> (!t1.(t1 -> (!t3.(!t2.(t2 t3) -> t3) t1)) t0))"],
122 | ["[pop pop]", "!t0.(t0 -> (!t3.(!t1.(t1 !t2.(t2 t3)) -> t3) t0))"],
123 | ["pop []", "!t1.(!t0.(t0 t1) -> (!t2.(t2 -> t2) t1))"],
124 | ["[] pop", "!t0.(t0 -> t0)"],
125 | ["[pop []]", "!t0.(t0 -> (!t2.(!t1.(t1 t2) -> (!t3.(t3 -> t3) t2)) t0))"],
126 | ["[] []", "!t0.(t0 -> (!t1.(t1 -> t1) (!t2.(t2 -> t2) t0)))"],
127 | ["[] dup", "!t0.(t0 -> (!t1.(t1 -> t1) (!t2.(t2 -> t2) t0)))"],
128 | ["[] dup dup", "!t0.(t0 -> (!t1.(t1 -> t1) (!t2.(t2 -> t2) (!t3.(t3 -> t3) t0))))"],
129 | ["dup pop", "!t0!t1.((t0 t1) -> (t0 t1))"],
130 |
131 | // Compositions of Primitives
132 | ["apply apply", "!t2.(!t0.((t0 -> !t1.((t1 -> t2) t1)) t0) -> t2)"],
133 | ["apply compose", "!t2!t3!t4.(!t0.((t0 -> !t1.((t1 -> t2) ((t3 -> t1) t4))) t0) -> ((t3 -> t2) t4))"],
134 | ["apply quote", "!t1!t2.(!t0.((t0 -> (t1 t2)) t0) -> (!t3.(t3 -> (t1 t3)) t2))"],
135 | ["apply dup", "!t1!t2.(!t0.((t0 -> (t1 t2)) t0) -> (t1 (t1 t2)))"],
136 | ["apply swap", "!t1!t2!t3.(!t0.((t0 -> (t1 (t2 t3))) t0) -> (t2 (t1 t3)))"],
137 | ["apply pop", "!t2.(!t0.((t0 -> !t1.(t1 t2)) t0) -> t2)"],
138 | ["compose apply", "!t1.(!t0.((t0 -> t1) !t2.((t2 -> t0) t2)) -> t1)"],
139 | ["compose compose", "!t1!t3!t4.(!t0.((t0 -> t1) !t2.((t2 -> t0) ((t3 -> t2) t4))) -> ((t3 -> t1) t4))"],
140 | ["compose quote", "!t1!t2!t3.(!t0.((t0 -> t1) ((t2 -> t0) t3)) -> (!t4.(t4 -> ((t2 -> t1) t4)) t3))"],
141 | ["compose dup", "!t1!t2!t3.(!t0.((t0 -> t1) ((t2 -> t0) t3)) -> ((t2 -> t1) ((t2 -> t1) t3)))"],
142 | ["compose swap", "!t1!t2!t3!t4.(!t0.((t0 -> t1) ((t2 -> t0) (t3 t4))) -> (t3 ((t2 -> t1) t4)))"],
143 | ["compose pop", "!t3.(!t0.(!t1.(t0 -> t1) (!t2.(t2 -> t0) t3)) -> t3)"],
144 | ["quote apply", "!t0!t1.((t0 t1) -> (t0 t1))"],
145 | ["quote compose", "!t0!t1!t2!t3.((t0 ((t1 -> t2) t3)) -> ((t1 -> (t0 t2)) t3))"],
146 | ["quote quote", "!t0!t1.((t0 t1) -> (!t2.(t2 -> (!t3.(t3 -> (t0 t3)) t2)) t1))"],
147 | ["quote dup", "!t0!t1.((t0 t1) -> (!t2.(t2 -> (t0 t2)) (!t3.(t3 -> (t0 t3)) t1)))"],
148 | ["quote swap", "!t0!t1!t2.((t0 (t1 t2)) -> (t1 (!t3.(t3 -> (t0 t3)) t2)))"],
149 | ["quote pop", "!t1.(!t0.(t0 t1) -> t1)"],
150 | ["dup apply", "!t1.(!t0.((((rec 1) t0) -> t1) t0) -> t1)"],
151 | ["dup compose", "!t0!t1.(((t0 -> t0) t1) -> ((t0 -> t0) t1))"],
152 | ["dup quote", "!t0!t1.((t0 t1) -> (!t2.(t2 -> (t0 t2)) (t0 t1)))"],
153 | ["dup dup", "!t0!t1.((t0 t1) -> (t0 (t0 (t0 t1))))"],
154 | ["dup swap", "!t0!t1.((t0 t1) -> (t0 (t0 t1)))"],
155 | ["dup pop", "!t0!t1.((t0 t1) -> (t0 t1))"],
156 | ["swap apply", "!t2.(!t0.(t0 !t1.(((t0 t1) -> t2) t1)) -> t2)"],
157 | ["swap compose", "!t0!t2!t3.(!t1.((t0 -> t1) ((t1 -> t2) t3)) -> ((t0 -> t2) t3))"],
158 | ["swap quote", "!t0!t1!t2.((t0 (t1 t2)) -> (!t3.(t3 -> (t1 t3)) (t0 t2)))"],
159 | ["swap dup", "!t0!t1!t2.((t0 (t1 t2)) -> (t1 (t1 (t0 t2))))"],
160 | ["swap swap", "!t0!t1!t2.((t0 (t1 t2)) -> (t0 (t1 t2)))"],
161 | ["swap pop", "!t0!t2.((t0 !t1.(t1 t2)) -> (t0 t2))"],
162 | ["pop apply", "!t2.(!t0.(t0 !t1.((t1 -> t2) t1)) -> t2)"],
163 | ["pop compose", "!t2!t3!t4.(!t0.(t0 !t1.((t1 -> t2) ((t3 -> t1) t4))) -> ((t3 -> t2) t4))"],
164 | ["pop quote", "!t1!t2.(!t0.(t0 (t1 t2)) -> (!t3.(t3 -> (t1 t3)) t2))"],
165 | ["pop dup", "!t1!t2.(!t0.(t0 (t1 t2)) -> (t1 (t1 t2)))"],
166 | ["pop swap", "!t1!t2!t3.(!t0.(t0 (t1 (t2 t3))) -> (t2 (t1 t3)))"],
167 | ["pop pop", "!t2.(!t0.(t0 !t1.(t1 t2)) -> t2)"],
168 |
169 | // Some standard library operators
170 | ["swap quote compose apply", "!t1!t2.(!t0.((t0 -> t1) (t2 t0)) -> (t2 t1))"], // Dip
171 |
172 | // Various tests
173 | ["swap quote compose apply pop", "!t1.(!t0.((t0 -> t1) !t2.(t2 t0)) -> t1)"], // Dip pop == pop apply
174 | ["[id] dup 0 swap apply swap [id] swap apply apply", "!t0.(t0 -> (Num t0))"], // Issue reported by StoryYeller
175 | ];
176 |
177 | function printCatTypes() {
178 | for (var k in catTypes) {
179 | var ts = catTypes[k];
180 | var t = parseType(ts);
181 | console.log(k);
182 | console.log(ts);
183 | console.log(t.toString());
184 | }
185 | }
186 |
187 | function testCatTermsCommon() {
188 | console.log("Testing Cat expression inference")
189 | let i = 0;
190 | for (var xs of catTests)
191 | runTest("Cat Inference Test " + i++,
192 | (x) => normalizeVarNames(inferCatType(x)).toString(),
193 | xs[0],
194 | xs[1]);
195 | }
196 |
197 | function issue1InCat() {
198 | var t = inferCatType("");
199 | t = alphabetizeVarNames(t) as TypeArray;
200 | console.log(t.toString());
201 |
202 | t = inferCatType("[id] dup [0 swap apply] swap quote compose apply [id] swap apply apply swap apply")
203 | t = alphabetizeVarNames(t) as TypeArray;
204 | console.log(t.toString());
205 | }
206 |
207 | /*
208 | function testCloning() {
209 | var types = catTesmap(t => inferCatType(t[0]));
210 | for (var t of types) {
211 | try {
212 | var r1 = ti.freshParameterNames(t, 0);
213 | var r2 = ti.freshVariableNames(t, 0);
214 | var r3 = t.clone({});
215 | compareTypes(t, r1);
216 | compareTypes(t, r2);
217 | compareTypes(t, r3);
218 | }
219 | catch (e) {
220 | console.log("FAILED cloning test of " + t + " with message " + e);
221 | }
222 | }
223 | }
224 | */
225 |
226 | const lambdaCatTests = [
227 | "\\a",
228 | "\\a \\b",
229 | "\\a \\b @a",
230 | "\\a \\b @b @a apply",
231 | "\\a \\b [@a] apply",
232 | "\\a @a @a",
233 | "\\a \\b @b @a",
234 | "\\a \\b @a @a",
235 | "\\a \\b \\c @c @a @b",
236 | "\\a \\b \\c @c @b @a",
237 | "\\a \\b \\c @a @b @c",
238 | "\\a \\b \\c @a @c @b",
239 | "\\a \\b \\c @b @a @c",
240 | "\\a \\b \\c @b @c @a",
241 | ];
242 |
243 | // SK Combinators expressed in Lambda Cat.
244 | const lambdaCatCombinators = {
245 | i : "\\x @x",
246 | k : "\\x [\\y @x]",
247 | s : "\\x [\\y [\\z @y @z apply @z @x apply apply]]",
248 | b : "\\x [\\y [\\z @z @y apply @x apply]]",
249 | c : "\\x [\\y [\\z @z @y @x apply apply]]",
250 | w : "\\x [\\y [@y @y @x apply apply]]",
251 | m : "\\x [@x @x apply]",
252 | issue : "[\\x @x] [\\i 0 @i apply [\\x @x] @i apply apply] apply"
253 | }
254 |
255 | export const lambdaTests = [
256 | ["(\\i.(i \\x.x) (i 0)) \\y.y", "Num"], // This cu\rrently fails: just like it does with HM
257 | ["(\\i.(i \\x.x) (i 0))", "?"], // This cu\rrently fails: just like it does with HM
258 | ["(\\i.(i 0)) \\y.y", "?"], // This cu\rrently fails: just like it does with HM
259 | ["(\\i.(i \\x.x)) \\y.y", "?"], // This cu\rrently fails: just like it does with HM
260 | ["0", "Num"],
261 | ["\\x.x", "!a.(a -> a)"],
262 | ["\\i.0", "!a.(a -> Num)"],
263 | ["\\i.i 0", "!a.((Num -> a) -> a)"],
264 | ["(\\i.0)", "!a.(a -> Num)"],
265 | ["(\\i.i 0)", "!a.((Num -> a) -> a)"],
266 | ["\\i.i (0)", "!a.((Num -> a) -> a)"],
267 | ["(\\i.0) \\y.y", "Num"],
268 | ["(\\i.0) (\\y.y)", "Num"],
269 | ["(\\i.i) \\y.y", "!a.(a -> a)"],
270 | ["(\\i.i) (\\y.y)", "!a.(a -> a)"],
271 | ["(\\i.i) (\\x.x) (\\y.y)","!a.(a -> a)"],
272 | ];
273 |
274 | function testRedex(x: Redex)
275 | {
276 | //var vars = Object.keys(freeVariables(x));
277 | //console.log("Free variables: " + vars);
278 |
279 | // TODO: proper alpha naming
280 |
281 | // TODO: validate this in isolation
282 | //var eta = etaConversion(x);
283 | //console.log("Eta reduction: " + eta);
284 |
285 | // TODO: valdiate this properly
286 | //var lift = lambdaLift(x);
287 | //console.log("Lambda lifting: " + lift);
288 |
289 | // Convert to SKI combinators
290 | //var elim = abstractionElimination(x);
291 | //console.log("Abstraction elimination: " + elim);
292 |
293 | // Produces very long terms
294 | //var combinator = combinatorToLambdaCalculus(elim);
295 | //console.log("As combinators: " + combinator);
296 | }
297 |
298 | function testLambdaToCat(s: string) {
299 | const redex = parseRedex(s);
300 | const xs = lambdaToPureCat(redex);
301 | console.log("Lambda expression: " + s);
302 | console.log("Cat expression: " + xs.join(' '));
303 | }
304 |
305 | function printCatType(s: string) {
306 | try {
307 | var t = inferCatType(s);
308 | console.log("Inferred type: " + t);
309 | }
310 | catch (e) {
311 | console.log("FAILED: " + e.message);
312 | }
313 | }
314 |
315 | function testCombinator(name: string, term: string, exp?: string) {
316 | try {
317 | console.log("Testing Lambda term " + name);
318 | const redex = parseRedex(term);
319 | console.log("Parsed redex: " + redex);
320 | testRedex(redex);
321 | const ys = lambdaToCat(redex);
322 | console.log("Lambda Cat: " + ys.join(' '));
323 | const xs = removeVars(ys);
324 | const cat = xs.join(' ');
325 | console.log("Pure Cat: " + cat);
326 | // TODO: test rewriting
327 | // TODO: test converting back
328 | const t = inferCatType(cat);
329 | console.log("Inferred type: " + t);
330 | console.log("Simplified type: " + typeToString(t));
331 | if (exp != null)
332 | console.log("Expected type: " + exp);
333 | }
334 | catch (e) {
335 | console.log("ERROR: " + e.message);
336 | }
337 | }
338 |
339 | function testCombinators() {
340 | for (var k in combinators)
341 | testCombinator(k, combinators[k]);
342 | }
343 |
344 | function testLambdaTerms() {
345 | for (var lt of lambdaTests)
346 | testCombinator(lt[0], lt[0], lt[1]);
347 | }
348 |
349 | const catTestStrings = [
350 | "[] dup",
351 | "[[] dup]",
352 | "[] [dup]",
353 | "[] [dup] apply",
354 | "dup [0] dip apply", // Is recursive. Not surprising.
355 | "dup [[0] dip apply []] dip", // what is type?
356 | "[] dup [[0] dip apply []] dip apply", // Should not fail
357 | "dup [[0] dip apply []] dip apply", // fails
358 | "dup [[0] dip apply []] dip apply apply",
359 | "[dup [[0] dip apply []] dip apply apply]",
360 | "[dup [[0] dip apply []] dip apply apply] apply",
361 | "[] [dup [[0] dip apply []] dip apply apply] apply",
362 | "[[0] dip apply []]",
363 | "[0] dip apply",
364 | "[0] dip",
365 | "[] []",
366 | "[] [0] dip",
367 | "[] dup [0] dip",
368 | "[] dup [0] dip apply",
369 | "[] dup [[0] dip apply []] dip",
370 | "[] dup [[0] dip apply []] dip apply",
371 | "[] dup [[0] dip apply []] dip apply apply",
372 | "[] [dup [[0] dip apply []] dip apply apply]",
373 | "[] [0] apply",
374 | "[] [[0] dip] apply",
375 | ];
376 |
377 | function testCatTermsWithVariance() {
378 | let i=0;
379 | for (var ct of catTestStrings) {
380 | runTest("Test Cat Inference with Variance " + i++,
381 | inferCatType,
382 | ct);
383 | }
384 | }
385 |
386 | testCatTermsCommon();
387 | testCatTermsWithVariance();
388 | testLambdaTerms();
389 |
390 | //testCombinators();
391 | //testLambdaTerms();
392 | // A B => A [B] apply
393 | // A B => [A] apply B
394 | // A B => A B apply
395 | //ti.setTrace(true);
396 |
397 | // Type of [dup [[0] dip apply []] dip apply apply] apply : !t1.((((Num !t0.((t0 -> t1) t0)) -> !t2.((t2 -> t1) t2)) !t3.((t3 -> t1) t3)) -> t1)
398 | // Type of [] [dup [[0] dip apply []] dip apply apply] : !t0.(t0 -> (!t2.((((Num !t1.((t1 -> t2) t1)) -> !t3.((t3 -> t2) t3)) !t4.((t4 -> t2) t4)) -> t2) (!t5.(t5 -> t5) t0)))
399 |
400 | declare var process;
401 | process.exit();
402 |
403 |
404 |
--------------------------------------------------------------------------------
/type-system.ts:
--------------------------------------------------------------------------------
1 | // A Type Inference Algorithm that provides support for full inference
2 | // of non-recursive higher rank polymorphic types.
3 | //
4 | // Copyright 2017 by Christopher Diggins
5 | // Licensed under the MIT License
6 |
7 | // Turn on for debugging purposes
8 | export var trace = false;
9 | export function setTrace(b: boolean) {
10 | trace = b;
11 | }
12 |
13 | //=========================================
14 | // Name generation
15 |
16 | // Used for generating new names
17 | var id=0;
18 |
19 | // Returns a new type variable.
20 | export function newTypeVar() : TypeVariable {
21 | return typeVariable("$" + id++);
22 | }
23 |
24 | //=========================================
25 | // Classes that represent kinds of types
26 |
27 | // Base class of a type: either a TypeArray, TypeVariable, or TypeConstant
28 | export class Type {
29 | // All type varible referenced somewhere by the type, or the type itself if it is a TypeVariable.
30 | typeVars : TypeVariable[] = [];
31 |
32 | clone(newTypes:ITypeLookup) : Type {
33 | throw new Error("Clone must be overridden in derived class");
34 | }
35 | }
36 |
37 | // A collection of a fixed number of types can be used to represent function types or tuple types.
38 | // A list of types is usually encoded as a nested set of type pairs (TypeArrays with two elements).
39 | // If a TypeArray has Type parameters, quantified unbound type variables, it is considered a "PolyType".
40 | // Binding type variables is done through the clone function
41 | export class TypeArray extends Type
42 | {
43 | constructor(
44 | public types : Type[], computeParameters:boolean)
45 | {
46 | super();
47 |
48 | // Compute all referenced types
49 | for (var t of types)
50 | this.typeVars = this.typeVars.concat(t.typeVars);
51 |
52 | // Given just a type with type variables the sete of type parameters
53 | // can be inferred based on where they occur in the type tree
54 | if (computeParameters)
55 | this.computeParameters();
56 | }
57 |
58 | // A helper function to copy a parameter list
59 | cloneParameters(dest:TypeArray, from:TypeVariable[], newTypes:ITypeLookup) {
60 | var params = [];
61 | for (var tv of from) {
62 | var param = newTypes[tv.name];
63 | if (param == undefined)
64 | throw new Error("Could not find type parameter: " + tv.name);
65 | params.push(param);
66 | }
67 | dest.typeParameterVars = params;
68 | }
69 |
70 | // Returns a copy of the type array, substituting type variables using the lookup table.
71 | clone(newTypes:ITypeLookup) : TypeArray {
72 | var r = new TypeArray(this.types.map(t => t.clone(newTypes)), false);
73 | this.cloneParameters(r, this.typeParameterVars, newTypes);
74 | return r;
75 | }
76 |
77 | freshVariableNames(id:number) : TypeArray {
78 | var newTypes:ITypeLookup = {};
79 | for (var t of descendantTypes(this))
80 | if (t instanceof TypeVariable)
81 | newTypes[t.name] = newTypeVar();
82 | return this.clone(newTypes);
83 | }
84 |
85 | // Returns a copy of the type array creating new parameter names.
86 | freshParameterNames() : TypeArray {
87 | // Create a lookup table for the type parameters with new names
88 | var newTypes:ITypeLookup = {};
89 | for (var tp of this.typeParameterNames)
90 | newTypes[tp] = newTypeVar();
91 |
92 | // Clone all of the types.
93 | var types = this.types.map(t => t.clone(newTypes));
94 |
95 | // Recursively call "freshParameterNames" on child type arrays as needed.
96 | types = types.map(t => t instanceof TypeArray ? t.freshParameterNames() : t);
97 | var r = new TypeArray(types, false);
98 |
99 | // Now recreate the type parameter list
100 | this.cloneParameters(r, this.typeParameterVars, newTypes);
101 |
102 | return r;
103 | }
104 |
105 | // A list of the parameter names (without repetition)
106 | get typeParameterNames() : string[] {
107 | return uniqueStrings(this.typeParameterVars.map(tv => tv.name)).sort();
108 | }
109 |
110 | // Infer which type variables are actually type parameters (universally quantified)
111 | // based on their position. Mutates in place.
112 | computeParameters() : TypeArray {
113 | this.typeParameterVars = [];
114 |
115 | // Recursively compute the parameters for base types
116 | this.types.forEach(t => { if (t instanceof TypeArray) t.computeParameters(); });
117 |
118 | for (var i=0; i < this.types.length; ++i) {
119 | var child = this.types[i];
120 |
121 | // Individual type variables are part of this scheme
122 | if (child instanceof TypeVariable)
123 | _reassignAllTypeVars(child.name, this);
124 | else
125 | if (child instanceof TypeArray) {
126 | // Get the vars of the child type.
127 | // If any of them show up in multiple child arrays, then they
128 | // are part of the parent's child
129 | for (var childVar of child.typeVars)
130 | if (_isTypeVarUsedElsewhere(this, childVar.name, i))
131 | _reassignAllTypeVars(childVar.name, this);
132 | }
133 | }
134 |
135 | // Implementation validation step:
136 | // Assure that the type scheme variables are all in the typeVars
137 | for (var v of this.typeParameterVars) {
138 | var i = this.typeVars.indexOf(v);
139 | if (i < 0)
140 | throw new Error("Internal error: type scheme references a variable that is not marked as referenced by the type variable")
141 | }
142 |
143 | return this;
144 | }
145 |
146 | // The type variables that are bound to this TypeArray.
147 | // Always a subset of typeVars. This could have the same type variable repeated twice.
148 | typeParameterVars : TypeVariable[] = [];
149 |
150 | // Provides a user friendly representation of the type scheme (list of type parameters)
151 | get typeParametersToString() : string {
152 | return this.isPolyType
153 | ? "!" + this.typeParameterNames.join("!") + "."
154 | : "";
155 | }
156 |
157 | // Returns true if there is at least one type parameter associated with this type array
158 | get isPolyType() : boolean {
159 | return this.typeParameterVars.length > 0;
160 | }
161 |
162 | // A user friendly name
163 | toString() : string {
164 | return this.typeParametersToString + "(" + this.types.join(' ') + ")";
165 | }
166 | }
167 |
168 | // A type variable is used for generics (e.g. T0, TR).
169 | // The type variable must belong to a type scheme of a polytype. This is like a "scope" for type variables.
170 | // Computing the type schema is done in an external function.
171 | export class TypeVariable extends Type
172 | {
173 | constructor(
174 | public name : string)
175 | {
176 | super();
177 | this.typeVars.push(this);
178 | }
179 |
180 | clone(newTypes:ITypeLookup) : Type {
181 | return this.name in newTypes
182 | ? newTypes[this.name] as TypeVariable
183 | : newTypes[this.name] = new TypeVariable(this.name);
184 | }
185 |
186 | toString() : string {
187 | return this.name;
188 | }
189 | }
190 |
191 | // A type constant is a fixed type (e.g. int, function). Also called a MonoType.
192 | export class TypeConstant extends Type
193 | {
194 | constructor(
195 | public name : string)
196 | { super(); }
197 |
198 | toString() : string {
199 | return this.name;
200 | }
201 |
202 | clone(newTypes:ITypeLookup) : TypeConstant {
203 | return new TypeConstant(this.name);
204 | }
205 | }
206 |
207 | //============================================================================
208 | // Helper classes and interfaces
209 |
210 | // A type unifier is a mapping from a type variable to a best-fit type
211 | export class TypeUnifier
212 | {
213 | constructor(
214 | public name:string,
215 | public unifier:Type)
216 | { }
217 | }
218 |
219 | // Given a type variable name finds the type set
220 | export interface ITypeUnifierLookup {
221 | [typeVarName:string] : TypeUnifier;
222 | }
223 |
224 | // Associates variable names with type expressions
225 | export interface ITypeLookup {
226 | [varName:string] : Type;
227 | }
228 |
229 | //=======================================================================
230 | // Various functions
231 |
232 | // This is helper function helps determine whether a type variable should belong
233 | export function _isTypeVarUsedElsewhere(t:TypeArray, varName:string, pos:number) : boolean {
234 | for (var i=0; i < t.types.length; ++i)
235 | if (i != pos && t.types[i].typeVars.some(v => v.name == varName))
236 | return true;
237 | return false;
238 | }
239 |
240 | // Associate the variable with a new type scheme. Removing it from the previous varScheme
241 | export function _reassignVarScheme(v:TypeVariable, t:TypeArray) {
242 | // Remove the variable from all other type schemes below the given one.
243 | for (var x of descendantTypes(t))
244 | if (x instanceof TypeArray)
245 | x.typeParameterVars = x.typeParameterVars.filter(vd => vd.name != v.name);
246 | t.typeParameterVars.push(v);
247 | }
248 |
249 | // Associate all variables of the given name in the TypeArray with the TypeArray's scheme
250 | export function _reassignAllTypeVars(varName:string, t:TypeArray) {
251 | t.typeVars.filter(v => v.name == varName).forEach(v => _reassignVarScheme(v, t));
252 | }
253 |
254 | export function replaceVarWithType(root:TypeArray, v:TypeVariable, r:Type) {
255 | // TODO: look for the variable in t. That would be recursive.
256 | if (root instanceof TypeArray) {
257 | // If we are replacing a "type parameter"
258 | root.typeParameterVars = root.typeParameterVars.filter(pv => !isTypeVariable(pv, v.name));
259 | for (var i=0; i < root.types.length; ++i) {
260 | const t = root.types[i];
261 | if (isTypeVariable(t, v.name))
262 | root.types[i] = freshParameterNames(r);
263 | else
264 | if (t instanceof TypeArray)
265 | replaceVarWithType(t, v, r);
266 | }
267 | }
268 | }
269 |
270 | //================================================
271 | // A classes used to implement unification.
272 |
273 | // Use this class to unify types that are constrained together.
274 | export class Unifier
275 | {
276 | // Given a type variable name find the unifier. Multiple type variables will map to the same unifier
277 | unifiers : ITypeUnifierLookup = {};
278 |
279 | // Unify both types, returning the most specific type possible.
280 | // When a type variable is unified with something the new unifier is stored.
281 | // Note: TypeFunctions and TypePairs ar handled as TypeArrays
282 | // * Constants are preferred over lists and variables
283 | // * Lists are preferred over variables
284 | // * Given two variables, the first one is chosen.
285 | unifyTypes(t1:Type, t2:Type, depth:number=0) : Type {
286 | if (!t1 || !t2)
287 | {
288 | throw new Error("Missing type expression");
289 | }
290 | if (t1 === t2)
291 | {
292 | return t1;
293 | }
294 | if (t1 instanceof TypeVariable)
295 | {
296 | let r = this._updateUnifier(t1, t2, depth);
297 | this._updateAllUnifiers(t1.name, t2);
298 | return r;
299 | }
300 | else if (t2 instanceof TypeVariable)
301 | {
302 | let r = this._updateUnifier(t2, t1, depth);
303 | this._updateAllUnifiers(t2.name, t1);
304 | return r;
305 | }
306 | else if (t1 instanceof TypeConstant && t2 instanceof TypeConstant)
307 | {
308 | if (t1.name != t2.name)
309 | return sumType([t1, t2]);
310 | else
311 | return t1;
312 | }
313 | else if (t1 instanceof TypeConstant || t2 instanceof TypeConstant)
314 | {
315 | return sumType([t1, t2]);
316 | }
317 | else if (t1 instanceof TypeArray && t2 instanceof TypeArray)
318 | {
319 | if (isSumType(t1) || isSumType(t2)) {
320 | return sumType([t1, t2]);
321 | }
322 |
323 | return this._unifyLists(t1, t2, depth+1);
324 | }
325 | throw new Error("Internal error, unexpected code path: " + t1 + " and " + t2);
326 | }
327 |
328 | // Debug function that dumps prints out a representation of the engine state.
329 | get state() : string {
330 | var results = [];
331 | for (var k in this.unifiers) {
332 | var u = this.unifiers[k];
333 | var t = u.unifier;
334 | results.push(`type unifier for ${ k }, unifier name ${ u.name }, unifying type ${t}`);
335 | }
336 | return results.join('\n');
337 | }
338 |
339 | // Replaces all variables in a type expression with the unified version
340 | // The previousVars variable allows detection of cyclical references
341 | getUnifiedType(expr:Type, previousVars:string[], unifiedVars:any) : Type {
342 | if (expr instanceof TypeConstant)
343 | return expr;
344 | else if (expr instanceof TypeVariable) {
345 | // If we encountered the type variable previously, it meant that there is a recursive relation
346 | for (var i=0; i < previousVars.length; ++i)
347 | if (previousVars[i] == expr.name)
348 | return recursiveType(i);
349 | var u = this.unifiers[expr.name];
350 | if (!u)
351 | return expr;
352 | // If the unifier is a type variable, we are done.
353 | else if (u.unifier instanceof TypeVariable)
354 | return u.unifier;
355 | else if (u.unifier instanceof TypeConstant)
356 | return u.unifier;
357 | else if (u.unifier instanceof TypeArray) {
358 | // TODO: this logic has to move into the unification step.
359 | if (u.name in unifiedVars) {
360 | // We have already seen this unified var before
361 | var u2 = u.unifier.freshParameterNames();
362 | return this.getUnifiedType(u2, [expr.name].concat(previousVars), unifiedVars);
363 | }
364 | else {
365 | unifiedVars[u.name] = 0;
366 | return this.getUnifiedType(u.unifier, [expr.name].concat(previousVars), unifiedVars);
367 | }
368 | }
369 | else
370 | throw new Error("Unhandled kind of type " + expr);
371 | }
372 | else if (expr instanceof TypeArray) {
373 | var types = expr.types.map(t => this.getUnifiedType(t, previousVars, unifiedVars));
374 | var r = new TypeArray(types, false);
375 | return r;
376 | }
377 | else
378 | throw new Error("Unrecognized kind of type expression " + expr);
379 | }
380 |
381 | // Choose one of two unifiers, or continue the unification process if necessary
382 | _chooseBestUnifier(t1:Type, t2:Type, depth:number) : Type {
383 | var r:Type;
384 | if (t1 instanceof TypeVariable && t2 instanceof TypeVariable)
385 | r = t1;
386 | else if (t1 instanceof TypeVariable)
387 | r = t2;
388 | else if (t2 instanceof TypeVariable)
389 | r = t1;
390 | else
391 | r = this.unifyTypes(t1, t2, depth+1);
392 | //if (trace) console.log(`Chose type for unification ${r} between ${t1} and ${t2} at depth ${depth}`)
393 | return r;
394 | }
395 |
396 | // Unifying lists involves unifying each element
397 | _unifyLists(list1:TypeArray, list2:TypeArray, depth:number) : TypeArray {
398 | if (list1.types.length != list2.types.length)
399 | throw new Error("Cannot unify differently sized lists: " + list1 + " and " + list2);
400 | var rtypes : Type[] = [];
401 | for (var i=0; i < list1.types.length; ++i)
402 | rtypes.push(this.unifyTypes(list1.types[i], list2.types[i], depth));
403 | // We just return the first list for now.
404 | return list1;
405 | }
406 |
407 | // All unifiers that refer to varName as the unifier are pointed to the new unifier
408 | _updateVariableUnifiers(varName:string, u:TypeUnifier) {
409 | for (var x in this.unifiers) {
410 | var t = this.unifiers[x].unifier;
411 | if (t instanceof TypeVariable)
412 | if (t.name == varName)
413 | this.unifiers[x] = u;
414 | }
415 | }
416 |
417 | // Go through a type and replace all instances of a variable with the new type
418 | // unless the new type is a variable.
419 | _replaceVarWithType(target:Type, varName:string, replace:Type) : Type {
420 | //if (trace) console.log("Replacing variable " + varName + " in target " + target + " with " + replace);
421 |
422 | // Just leave it as is.
423 | // Replacing a variable with a variable is kind of meaningless.
424 | if (replace instanceof TypeVariable)
425 | return target;
426 |
427 | // Create new parameter names as needed
428 | if (replace instanceof TypeArray)
429 | {
430 | if (replace.isPolyType) {
431 | // Get some new parameters for the poly type
432 | replace = freshParameterNames(replace);
433 | }
434 | }
435 |
436 | // Look at the target type and decide what to do.
437 | if (target instanceof TypeVariable) {
438 | if (target.name == varName)
439 | return replace;
440 | else
441 | return target;
442 | }
443 | else if (target instanceof TypeConstant) {
444 | return target;
445 | }
446 | else if (target instanceof TypeArray) {
447 | // TODO?: look at the parameters. Am I replacing a parameter? If so, throw it out.
448 | // BUT!!: I don't think I have to do this step, because at the end the type will be constructed correctly.
449 | return target.clone({varName:replace});
450 | }
451 | else {
452 | throw new Error("Unrecognized kind of type " + target);
453 | }
454 | }
455 |
456 | // Returns all of the unifiers as an array
457 | get _allUnifiers() : TypeUnifier[] {
458 | var r : TypeUnifier[] = [];
459 | for (var k in this.unifiers)
460 | r.push(this.unifiers[k]);
461 | return r;
462 | }
463 |
464 | // Update all unifiers once I am making a replacement
465 | _updateAllUnifiers(a:string, t:Type)
466 | {
467 | for (var tu of this._allUnifiers)
468 | tu.unifier = this._replaceVarWithType(tu.unifier, a, t);
469 | }
470 |
471 | // Computes the best unifier between the current unifier and the new variable.
472 | // Updates all unifiers which point to a (or to t if t is a TypeVar) to use the new type.
473 | _updateUnifier(a:TypeVariable, t:Type, depth:number) : Type {
474 | var u = this._getOrCreateUnifier(a);
475 | if (t instanceof TypeVariable)
476 | t = this._getOrCreateUnifier(t).unifier;
477 |
478 | u.unifier = this._chooseBestUnifier(u.unifier, t, depth);
479 | this._updateVariableUnifiers(a.name, u);
480 | if (t instanceof TypeVariable)
481 | this._updateVariableUnifiers(t.name, u);
482 |
483 | return u.unifier;
484 | }
485 |
486 | // Gets or creates a type unifiers for a type variables
487 | _getOrCreateUnifier(t : TypeVariable) : TypeUnifier {
488 | if (!(t.name in this.unifiers))
489 | return this.unifiers[t.name] = new TypeUnifier(t.name, t);
490 | else
491 | return this.unifiers[t.name];
492 | }
493 | }
494 |
495 | //======================================================================================
496 | // Helper functions
497 |
498 | // Creates a type list as nested pairs ("cons" cells ala lisp).
499 | // The last type is assumed to be a row variable.
500 | export function rowPolymorphicList(types:Type[]) : Type {
501 | if (types.length == 0)
502 | throw new Error("Expected a type list with at least one type variable")
503 | else if (types.length == 1) {
504 | if (types[0] instanceof TypeVariable)
505 | return types[0];
506 | else
507 | throw new Error("Expected a row variable in the final position");
508 | }
509 | else
510 | return typeArray([types[0], rowPolymorphicList(types.slice(1))]);
511 | }
512 |
513 | // Creates a row-polymorphic function type: adding the implicit row variable
514 | export function rowPolymorphicFunction(inputs:Type[], outputs:Type[]) : TypeArray {
515 | var row = typeVariable('_');
516 | inputs.push(row);
517 | outputs.push(row);
518 | return functionType(rowPolymorphicList(inputs), rowPolymorphicList(outputs));
519 | }
520 |
521 | // Creates a type array from an array of types
522 | export function typeArray(types:Type[]) : TypeArray {
523 | return new TypeArray(types, true);
524 | }
525 |
526 | // Creates a type constant
527 | export function typeConstant(name:string) : TypeConstant {
528 | return new TypeConstant(name);
529 | }
530 |
531 | // Creates a type variable
532 | export function typeVariable(name:string) : TypeVariable {
533 | return new TypeVariable(name);
534 | }
535 |
536 | // Creates a function type, as a special kind of a TypeArray
537 | export function functionType(input: Type, output: Type) : TypeArray {
538 | return typeArray([input, typeConstant('->'), output]);
539 | }
540 |
541 | // Creates a sum type. If any of the types in the array are a sumType, it is flattened.
542 | export function sumType(types: Type[]) : TypeArray {
543 | let r: Type[] = [];
544 | for (let t of types)
545 | if (isSumType(t))
546 | r.push(...sumTypeOptions(t));
547 | else
548 | r.push(t);
549 | return typeArray([typeConstant('|'), typeArray(r)]);
550 | }
551 |
552 | // Creates an array type, as a special kind of TypeArray
553 | export function arrayType(element:Type) : TypeArray {
554 | return typeArray([element, typeConstant('[]')]);
555 | }
556 |
557 | // Creates a list type, as a special kind of TypeArray
558 | export function listType(element:Type) : TypeArray {
559 | return typeArray([element, typeConstant('*')]);
560 | }
561 |
562 | // Creates a recursive type, as a special kind of TypeArray. The numberical value
563 | // refers to the depth of the recursion: how many TypeArrays you have to go up
564 | // to find the recurison base case.
565 | export function recursiveType(depth:Number) : TypeArray {
566 | return typeArray([typeConstant('rec'), typeConstant(depth.toString())]);
567 | }
568 |
569 | // Returns true if and only if the type is a type constant with the specified name
570 | export function isTypeConstant(t:Type, name:string) : boolean {
571 | return t instanceof TypeConstant && t.name === name;
572 | }
573 |
574 | // Returns true if and only if the type is a type constant with the specified name
575 | export function isTypeVariable(t:Type, name:string) : boolean {
576 | return t instanceof TypeVariable && t.name === name;
577 | }
578 |
579 | // Returns true if any of the types are the type variable
580 | export function variableOccurs(name:string, type:Type) : boolean {
581 | return descendantTypes(type).some(t => isTypeVariable(t, name));
582 | }
583 |
584 | // Returns true if and only if the type is a type constant with the specified name
585 | export function isTypeArray(t:Type, name:string) : boolean {
586 | return t instanceof TypeArray && t.types.length == 2 && isTypeConstant(t.types[1], '[]');
587 | }
588 |
589 | // Returns true iff the type is a TypeArary representing a function type
590 | export function isFunctionType(t:Type) : boolean {
591 | return t instanceof TypeArray && t.types.length == 3 && isTypeConstant(t.types[1], '->');
592 | }
593 |
594 | // Returns true iff the type is a TypeArary representing a sum type
595 | export function isSumType(t:Type) : boolean {
596 | return t instanceof TypeArray && t.types.length == 2 && isTypeConstant(t.types[0], '|');
597 | }
598 |
599 | export function sumTypeOptions(t:Type): Type[] {
600 | if (!isSumType(t)) throw new Error("Expected a sum type");
601 | return ((t as TypeArray).types[1] as TypeArray).types;
602 | }
603 |
604 | // Returns the input types (argument types) of a TypeArray representing a function type
605 | export function functionInput(t:Type) : Type {
606 | if (!isFunctionType(t)) throw new Error("Expected a function type");
607 | return (t as TypeArray).types[0];
608 | }
609 |
610 | // Returns the output types (return types) of a TypeArray representing a function type
611 | export function functionOutput(t:Type) : Type {
612 | if (!isFunctionType(t)) throw new Error("Expected a function type");
613 | return (t as TypeArray).types[2];
614 | }
615 |
616 | // Returns all types contained in this type
617 | export function descendantTypes(t:Type, r:Type[] = []) : Type[] {
618 | r.push(t);
619 | if (t instanceof TypeArray)
620 | t.types.forEach(t2 => descendantTypes(t2, r));
621 | return r;
622 | }
623 |
624 | // Returns true if the type is a polytype
625 | export function isPolyType(t:Type) {
626 | return t instanceof TypeArray && t.typeParameterVars.length > 0;
627 | }
628 |
629 | // Returns true if the type is a function that generates a polytype.
630 | export function generatesPolytypes(t:Type) : boolean {
631 | if (!isFunctionType(t))
632 | return false;
633 | return descendantTypes(functionOutput(t)).some(isPolyType);
634 | }
635 |
636 | // Global function for fresh variable names
637 | export function freshVariableNames(t:Type, id:number) : Type {
638 | return (t instanceof TypeArray) ? t.freshVariableNames(id) : t;
639 | }
640 |
641 | // Global function for fresh parameter names
642 | export function freshParameterNames(t:Type) : Type {
643 | return (t instanceof TypeArray) ? t.freshParameterNames() : t;
644 | }
645 |
646 | export function computeParameters(t:Type) : Type {
647 | return (t instanceof TypeArray) ? t.computeParameters() : t;
648 | }
649 |
650 | //========================================================
651 | // Variable name functions
652 |
653 | // Rename all type variables os that they follow T0..TN according to the order the show in the tree.
654 | export function normalizeVarNames(t:Type) : Type {
655 | var names = {};
656 | var count = 0;
657 | for (var dt of descendantTypes(t))
658 | if (dt instanceof TypeVariable)
659 | if (!(dt.name in names))
660 | names[dt.name] = typeVariable("t" + count++);
661 | return t.clone(names);
662 | }
663 |
664 | // Converts a number to a letter from 'a' to 'z'.
665 | function numberToLetters(n:number) : string {
666 | return String.fromCharCode(97 + n);
667 | }
668 |
669 | // Rename all type variables so that they are alphabetical in the order they occur in the tree
670 | export function alphabetizeVarNames(t:Type) : Type {
671 | var names = {};
672 | var count = 0;
673 | for (var dt of descendantTypes(t))
674 | if (dt instanceof TypeVariable)
675 | if (!(dt.name in names))
676 | names[dt.name] = typeVariable(numberToLetters(count++));
677 | return t.clone(names);
678 | }
679 |
680 | // Compares whether two types are the same after normalizing the type variables.
681 | export function areTypesSame(t1:Type, t2:Type) {
682 | var s1 = normalizeVarNames(t1).toString();
683 | var s2 = normalizeVarNames(t2).toString();
684 | return s1 === s2;
685 | }
686 |
687 | export function variableOccursOnInput(varName:string, type:TypeArray) {
688 | for (var t of descendantTypes(type)) {
689 | if (isFunctionType(t)) {
690 | var input = functionInput(type);
691 | if (variableOccurs(varName, input)) {
692 | return true;
693 | }
694 | }
695 | }
696 | }
697 |
698 | // Returns true if and only if the type is valid
699 | export function isValid(type:Type) {
700 | for (var t of descendantTypes(type)) {
701 | if (isTypeConstant(t, "rec")) {
702 | return false;
703 | }
704 | else if (t instanceof TypeArray) {
705 | if (isFunctionType(t))
706 | for (var p of t.typeParameterNames)
707 | if (!variableOccursOnInput(p, t))
708 | return false;
709 | }
710 | }
711 | return true;
712 | }
713 |
714 | //============================================================
715 | // Top level type operations
716 | // - Composition
717 | // - Quotation
718 |
719 | // Returns the function type that results by composing two function types
720 | export function composeFunctions(f:TypeArray, g:TypeArray) : TypeArray {
721 | if (!isFunctionType(f)) throw new Error("Expected a function type for f");
722 | if (!isFunctionType(g)) throw new Error("Expected a function type for g");
723 |
724 | f = f.freshVariableNames(0) as TypeArray;
725 | g = g.freshVariableNames(1) as TypeArray;
726 |
727 | if (trace) {
728 | console.log("f: " + f);
729 | console.log("g: " + g);
730 | }
731 |
732 | var inF = functionInput(f);
733 | var outF = functionOutput(f);
734 | var inG = functionInput(g);
735 | var outG = functionOutput(g);
736 |
737 | var e = new Unifier();
738 | e.unifyTypes(outF, inG);
739 | var input = e.getUnifiedType(inF, [], {});
740 | var output = e.getUnifiedType(outG, [], {});
741 |
742 | var r = functionType(input, output);
743 | if (trace) {
744 | console.log(e.state);
745 | console.log("Intermediate result: " + r)
746 | }
747 |
748 | // Recompute parameters.
749 | r.computeParameters();
750 | if (trace) {
751 | console.log("Final result: " + r);
752 | }
753 | r = normalizeVarNames(r) as TypeArray;
754 | return r;
755 | }
756 |
757 | // Composes a chain of functions
758 | export function composeFunctionChain(fxns:TypeArray[]) : TypeArray {
759 | if (fxns.length == 0)
760 | return idFunction();
761 | let t = fxns[0];
762 | for (let i=1; i < fxns.length; ++i)
763 | t = composeFunctions(t, fxns[i]);
764 | return t;
765 | }
766 |
767 | // Composes a chain of functions in reverse. Should give the same result
768 | export function composeFunctionChainReverse(fxns:TypeArray[]) : TypeArray {
769 | if (fxns.length == 0)
770 | return idFunction();
771 | let t = fxns[fxns.length - 1];
772 | for (let i=fxns.length-2; i >= 0; --i)
773 | t = composeFunctions(fxns[i], t);
774 | return t;
775 | }
776 |
777 | // Creates a function type that generates the given type.
778 | // If given no type returns the empty quotation.
779 | export function quotation(x: Type) : TypeArray {
780 | const row = typeVariable('_');
781 | x = freshParameterNames(x);
782 | var r = functionType(row, x ? typeArray([x, row]) : row);
783 | r.computeParameters();
784 | r = normalizeVarNames(r) as TypeArray;
785 | return r;
786 | }
787 |
788 | // Returns the type of the id function
789 | export function idFunction() : TypeArray {
790 | return quotation(null);
791 | }
792 |
793 | //=====================================================================
794 | // General purpose utility functions
795 |
796 | // Returns only the uniquely named strings
797 | export function uniqueStrings(xs:string[]) : string[] {
798 | var r = {};
799 | for (var x of xs)
800 | r[x] = true;
801 | return Object.keys(r);
802 | }
803 |
804 | //================================================================
805 | // Pretty type formatting.
806 |
807 | function flattenFunctionIO(t: Type): Type[] {
808 | if (t instanceof TypeArray) {
809 | return [t.types[0], ...flattenFunctionIO(t.types[1])];
810 | }
811 | else {
812 | return [t];
813 | }
814 | }
815 |
816 | function functionInputToString(t: Type): string {
817 | return flattenFunctionIO(functionInput(t)).map(typeToString).join(' ')
818 | }
819 |
820 | function functionOutputToString(t: Type): string {
821 | return flattenFunctionIO(functionOutput(t)).map(typeToString).join(' ')
822 | }
823 |
824 | export function typeToString(t: Type): string {
825 | if (isFunctionType(t)) {
826 | return "(" + functionInputToString(t) + " -> " + functionOutputToString(t) + ")";
827 | }
828 | else if (t instanceof TypeVariable) {
829 | return t.name;
830 | }
831 | else if (t instanceof TypeConstant) {
832 | return t.name;
833 | }
834 | else if (t instanceof TypeArray) {
835 | return "[" + t.types.map(typeToString).join(' ') + "]";
836 | }
837 | }
838 |
--------------------------------------------------------------------------------