├── tsconfig.json ├── .vscode ├── tasks.json └── launch.json ├── docs └── index.html ├── package.json ├── cat-library.ts ├── LICENSE ├── lambda-calculus-to-cat.ts ├── type-parser.ts ├── cat-types.ts ├── scoped-hm-type-inference.ts ├── cat-rewrite.ts ├── cat-parser.ts ├── combinatory-logic.ts ├── notes.txt ├── lambda-calculus.ts ├── cat-lambda.ts ├── README.md ├── test.ts └── type-system.ts /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "target": "ES5", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "outDir": "build", 8 | }, 9 | "include": [ 10 | "*.ts" 11 | ], 12 | "exclude": [ 13 | "node_modules" 14 | ] 15 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | 4 | // The command is tsc. 5 | "command": "tsc", 6 | 7 | // Show the output window only if unrecognized errors occur. 8 | "showOutput": "always", 9 | 10 | // Under windows use tsc.exe. This ensures we don't need a shell. 11 | "windows": { 12 | "command": "tsc" 13 | }, 14 | 15 | // args is the program to compile. 16 | "args": [], 17 | 18 | "isShellCommand": true, 19 | 20 | // use the standard tsc problem matcher to find compile problems 21 | // in the output. 22 | "problemMatcher": "$tsc" 23 | } -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Cat Programming Language 4 | 5 | 6 | 7 | 8 | 9 | 10 |

Cat Programming Language

11 | 12 |

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 | ![image](https://user-images.githubusercontent.com/1759994/236684768-3c352cc6-4abd-4add-a35d-247caa978e62.png) 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 | ![image](https://user-images.githubusercontent.com/1759994/236686865-541cd729-574a-4dad-91a2-2a34d41a2347.png) 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 | --------------------------------------------------------------------------------