Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit 91a137d923e7ee0dfb123498244f73114e8f5033 0 parents
@puffnfresh authored
3  .hgignore
@@ -0,0 +1,3 @@
+node_modules
+src/parser.js
+examples/.*\.js
2  Makefile
@@ -0,0 +1,2 @@
+all:
+ node src/grammar.js
27 README.md
@@ -0,0 +1,27 @@
+Roy
+===
+
+Roy rhymes with "toy" and that's what it is. This is a small functional language that compiles to JavaScript. It has two main features:
+
+* Damas-Hindley-Milner type inference
+* Whitespace significant syntax
+
+It is mainly limited to being a toy because it can't interface with JS completely. Gluing JS semantics with static typing is a hard problem that I'm working on.
+
+Example
+---
+
+Input (test.roy):
+
+ let addTwo n =
+ n + 2
+
+ console.log (addTwo 40)
+
+Output (test.js):
+
+ "use strict";
+ var addTwo = function(n) {return n + 2;};
+ console.log(addTwo(40))
+
+Calling `addTwo "test"` will result in a compile-time error.
6 examples/console.roy
@@ -0,0 +1,6 @@
+console.log "One"
+console.info "Two"
+console.warn "Three"
+console.error "Four"
+console.trace "Five"
+console.assert "Six"
12 examples/funcs.roy
@@ -0,0 +1,12 @@
+let print x =
+ console.log x
+
+print "Hello"
+print 100
+print [1, 2, 3]
+
+let log = console.log
+
+log "Hello"
+log 100
+log [1, 2, 3]
7 examples/gcd.roy
@@ -0,0 +1,7 @@
+let gcd a b =
+ if b == 0 then
+ a
+ else
+ gcd b (a % b)
+
+console.log (gcd 49 35)
2  examples/helloworld.roy
@@ -0,0 +1,2 @@
+// Comment
+console.log "Hello world"
13 examples/types.roy
@@ -0,0 +1,13 @@
+// Strong
+console.log 40 + 2
+
+// Won't compile:
+// console.log "40" + 2
+
+// Explicit
+let f x : Number = x
+
+console.log (f 100)
+
+// Won't compile:
+// console.log (f "100")
10 roy
@@ -0,0 +1,10 @@
+#!/usr/bin/env node
+
+try {
+ require('./src/parser');
+} catch(e) {
+ console.log('parser module not found. Must run `make` first.');
+ return;
+}
+
+require('./src/compile');
131 src/compile.js
@@ -0,0 +1,131 @@
+var fs = require('fs'),
+ typecheck = require('./typeinference').typecheck,
+ nodes = require('./nodes').nodes,
+ types = require('./types'),
+ parser = require('./parser').parser,
+ lexer = require('./lexer');
+
+// Assigning the nodes to `parser.yy` allows the grammar to access the nodes from
+// the `yy` namespace.
+parser.yy = nodes;
+
+parser.lexer = {
+ "lex": function() {
+ var token = this.tokens[this.pos] ? this.tokens[this.pos++] : ['EOF'];
+ this.yytext = token[1];
+ this.yylineno = token[2];
+ return token[0];
+ },
+
+ "setInput": function(tokens) {
+ this.tokens = tokens;
+ this.pos = 0;
+ },
+
+ "upcomingInput": function() {
+ return "";
+ }
+};
+
+// Compile an abstract syntax tree (AST) node to JavaScript.
+var compile = function(n) {
+ return n.accept({
+ // Function definition to JavaScript function.
+ visitFunction: function() {
+ var getArgs = function(a) {
+ return a.map(function(v) {
+ return v.name;
+ }).join(", ");
+ };
+ var compiledBody = n.body.map(compile);
+ var initString = compiledBody.slice(0, compiledBody.length - 1).join('');
+ var lastString = compiledBody[compiledBody.length - 1];
+ return "var " + n.name + " = function(" + getArgs(n.args) + ") {" + initString + "return " + lastString + ";};";
+ },
+ visitIfThenElse: function() {
+ var compiledCondition = compile(n.condition);
+ var compiledIfTrue = n.ifTrue.map(compile).join('');
+ var compiledIfFalse = n.ifFalse.map(compile).join('');
+ return "(function(){if(" + compiledCondition + "){return " + compiledIfTrue + "}else{return " + compiledIfFalse + "}})();";
+ },
+ // Let binding to JavaScript variable.
+ visitLet: function() {
+ return "var " + n.name + " = " + compile(n.value) + ";";
+ },
+ // Call to JavaScript call.
+ visitCall: function() {
+ return compile(n.func) + "(" + n.args.map(compile).join(", ") + ")";
+ },
+ visitAccess: function() {
+ return compile(n.value) + "." + n.property;
+ },
+ visitOperator: function() {
+ return [compile(n.left), n.name, compile(n.right)].join(" ");
+ },
+ // Print all other nodes directly.
+ visitComment: function() {
+ return n.value;
+ },
+ visitIdentifier: function() {
+ return n.value;
+ },
+ visitNumber: function() {
+ return n.value;
+ },
+ visitString: function() {
+ return n.value;
+ },
+ visitBoolean: function() {
+ return n.value;
+ },
+ visitArray: function() {
+ return '[' + n.values.map(compile).join(', ') + ']';
+ },
+ visitObject: function() {
+ var key;
+ var pairs = [];
+ for(key in n.values) {
+ pairs.push(key + ": " + compile(n.values[key]));
+ }
+ return "{" + pairs.join(", ") + "}";
+ }
+ });
+};
+
+if(process.argv.length < 3) {
+ console.log('You must give a .roy file as an argument.');
+ return;
+}
+
+// Read the file content.
+var filename = process.argv[2];
+var source = fs.readFileSync(filename, 'utf8');
+//console.log(source);
+// Parse the file to an AST.
+var tokens = lexer.tokenise(source);
+//console.log(tokens);
+var ast = parser.parse(tokens);
+//console.log(ast);
+
+// Typecheck the AST. Any type errors will throw an exception.
+var typeA = new types.Variable();
+var typeB = new types.Variable();
+typecheck(ast, {
+ 'console': new types.ObjectType({
+ 'log': new types.FunctionType([typeA, typeB]),
+ 'info': new types.FunctionType([typeA, typeB]),
+ 'warn': new types.FunctionType([typeA, typeB]),
+ 'error': new types.FunctionType([typeA, typeB]),
+ 'trace': new types.FunctionType([typeA, typeB]),
+ 'assert': new types.FunctionType([typeA, typeB])
+ })
+});
+
+// Output strict JavaScript.
+var output = ['"use strict";'];
+ast.forEach(function(v) {
+ output.push(compile(v));
+});
+
+// Write the JavaScript output.
+fs.writeFile(filename.replace(/roy$/, 'js'), output.join('\n'), 'utf8');
117 src/grammar.js
@@ -0,0 +1,117 @@
+var sys = require('sys'),
+ Parser = require('jison').Parser;
+
+var grammar = {
+ "startSymbol": "program",
+
+ "operators": [
+ ["left", "=="],
+ ["left", "+", "-"],
+ ["left", "%"],
+ ["left", "."]
+ ],
+
+ "bnf": {
+ "program": [
+ ["EOF", "return [];"],
+ ["body EOF", "return $1;"]
+ ],
+ "body": [
+ ["line", "$$ = [$1];"],
+ ["body TERMINATOR line", "$$ = $1; $1.push($3);"],
+ ["body TERMINATOR", "$$ = $1;"]
+ ],
+ "line": [
+ ["statement", "$$ = $1;"],
+ ["expression", "$$ = $1;"],
+ ["COMMENT", "$$ = new yy.Comment($1);"]
+ ],
+ "block": [
+ ["INDENT body OUTDENT", "$$ = $2;"],
+ ["INDENT OUTDENT", "$$ = $1;"]
+ ],
+ "statement": [
+ ["letFunction", "$$ = $1;"],
+ ["letBinding", "$$ = $1;"]
+ ],
+ "expression": [
+ ["innerExpression", "$$ = $1;"],
+ ["call", "$$ = $1;"]
+ ],
+ "innerExpression": [
+ ["ifThenElse", "$$ = $1;"],
+ ["( expression )", "$$ = $2;"],
+ ["accessor", "$$ = $1;"],
+ ["innerExpression + innerExpression", "$$ = new yy.Operator($2, $1, $3);"],
+ ["innerExpression % innerExpression", "$$ = new yy.Operator($2, $1, $3);"],
+ ["innerExpression == innerExpression", "$$ = new yy.Operator($2, $1, $3);"],
+ ["literal", "$$ = $1;"]
+ ],
+ "ifThenElse": [
+ ["IF innerExpression THEN block TERMINATOR ELSE block", "$$ = new yy.IfThenElse($2, $4, $7);"]
+ ],
+ "letFunction": [
+ ["LET IDENTIFIER paramList optType = block", "$$ = new yy.Function($2, $3, $6, $4);"],
+ ["LET IDENTIFIER paramList optType = expression", "$$ = new yy.Function($2, $3, [$6], $4);"]
+ ],
+ "letBinding": [
+ ["LET IDENTIFIER optType = expression", "$$ = new yy.Let($2, $5, $3);"],
+ ["LET IDENTIFIER optType = INDENT expression OUTDENT", "$$ = new yy.Let($2, $6, $3);"]
+ ],
+ "paramList": [
+ ["param", "$$ = [$1];"],
+ ["paramList param", "$$ = $1; $1.push($2);"]
+ ],
+ "param": [
+ ["IDENTIFIER", "$$ = new yy.Arg($1);"],
+ ["( IDENTIFIER : identifier )", "$$ = new yy.Arg($2, $4);"]
+ ],
+ "optType": [
+ ["", ""],
+ [": identifier", "$$ = $2"]
+ ],
+ "call": [
+ ["accessor argList", "$$ = new yy.Call($1, $2);"],
+ ["( expression ) argList", "$$ = new yy.Call($2, $4);"]
+ ],
+ "argList": [
+ ["innerExpression", "$$ = [$1];"],
+ ["argList innerExpression", "$$ = $1; $1.push($2);"]
+ ],
+ "literal": [
+ ["NUMBER", "$$ = new yy.Number($1);"],
+ ["STRING", "$$ = new yy.String($1);"],
+ ["BOOLEAN", "$$ = new yy.Boolean($1);"],
+ ["[ optValues ]", "$$ = new yy.Array($2);"],
+ ["{ optPairs }", "$$ = new yy.Object($2);"]
+ ],
+ "optValues": [
+ ["", "$$ = [];"],
+ ["arrayValues", "$$ = $1;"]
+ ],
+ "arrayValues": [
+ ["expression", "$$ = [$1];"],
+ ["arrayValues , expression", "$$ = $1; $1.push($3);"]
+ ],
+ "optPairs": [
+ ["", "$$ = {};"],
+ ["keyPairs", "$$ = $1;"]
+ ],
+ "keyPairs": [
+ ["STRING : expression", "$$ = {}; $$[$1] = $3;"],
+ ["keyPairs , STRING : expression", "$$ = $1; $1[$3] = $5;"]
+ ],
+ "accessor": [
+ ["IDENTIFIER", "$$ = new yy.Identifier($1);"],
+ ["accessor . IDENTIFIER", "$$ = new yy.Access($1, $3);"]
+ ],
+ "identifier": [
+ ["IDENTIFIER", "$$ = new yy.Identifier($1);"]
+ ]
+ }
+};
+
+var parser = new Parser(grammar, {debug: true});
+
+var fs = require('fs');
+fs.writeFile('src/parser.js', parser.generate());
192 src/lexer.js
@@ -0,0 +1,192 @@
+var IDENTIFIER = /^[a-zA-Z$_][a-zA-Z0-9$_]*/;
+var NUMBER = /^-?[0-9]+(\.[0-9]+)?/;
+var COMMENT = /^\/\/.*/;
+var WHITESPACE = /^[^\n\S]+/;
+var INDENT = /^(?:\n[^\n\S]*)+/;
+
+var chunk;
+
+var indent = 0;
+var indents = []
+var tokens = [];
+
+var identifierToken = function() {
+ var value,
+ name,
+ token = IDENTIFIER.exec(chunk);
+ if(token) {
+ value = token[0];
+ switch(value) {
+ case 'let':
+ name = 'LET';
+ break;
+ case 'if':
+ name = 'IF';
+ break;
+ case 'then':
+ name = 'THEN';
+ break;
+ case 'else':
+ name = 'ELSE';
+ break;
+ case 'true':
+ case 'false':
+ name = 'BOOLEAN';
+ break;
+ default:
+ name = 'IDENTIFIER'
+ break;
+ }
+ tokens.push([name, value]);
+ return token[0].length;
+ }
+
+ return 0;
+};
+
+var numberToken = function() {
+ var token = NUMBER.exec(chunk);
+ if(token) {
+ tokens.push(['NUMBER', token[0]]);
+ return token[0].length;
+ }
+
+ return 0;
+};
+
+var stringToken = function() {
+ var firstChar = chunk.charAt(0),
+ quoted = false,
+ nextChar;
+ if(firstChar == '"' || firstChar == "'") {
+ for(var i = 1; i < chunk.length; i++) {
+ if(!quoted) {
+ nextChar = chunk.charAt(i);
+ if(nextChar == "\\") {
+ quoted = true;
+ } else if(nextChar == firstChar) {
+ tokens.push(['STRING', chunk.substring(0, i + 1)]);
+ return i + 1;
+ }
+ } else {
+ quoted = false;
+ }
+ }
+ }
+
+ return 0;
+};
+
+var commentToken = function() {
+ var token = COMMENT.exec(chunk);
+ if(token) {
+ tokens.push(['COMMENT', token[0]]);
+ return token[0].length;
+ }
+
+ return 0;
+};
+
+var whitespaceToken = function() {
+ var token = WHITESPACE.exec(chunk);
+ if(token) {
+ return token[0].length;
+ }
+
+ return 0;
+};
+
+var lineToken = function() {
+ var token = INDENT.exec(chunk);
+ if(token) {
+ var lastNewline = token[0].lastIndexOf("\n") + 1;
+ var size = token[0].length - lastNewline;
+ if(size > indent) {
+ indents.push(size);
+ tokens.push(['INDENT', size - indent]);
+ } else {
+ if(size < indent) {
+ var last = indents[indents.length - 1];
+ while(size < last) {
+ tokens.push(['OUTDENT', last - size]);
+ indents.pop();
+ last = indents[indents.length - 1];
+ }
+ }
+ tokens.push(['TERMINATOR', token[0].substring(0, lastNewline)]);
+ }
+ indent = size;
+ return token[0].length;
+ }
+
+ return 0;
+};
+
+var literalToken = function() {
+ var tag = chunk.slice(0, 1);
+ var next;
+ switch(tag) {
+ case '=':
+ var next = chunk.slice(0, 2);
+ switch(next) {
+ case '==':
+ tokens.push([next, next]);
+ return 2;
+ }
+ tokens.push([tag, tag]);
+ return 1;
+ case ':':
+ case '.':
+ case ',':
+ case '+':
+ case '-':
+ case '*':
+ case '/':
+ case '%':
+ case '[':
+ case ']':
+ case '{':
+ case '}':
+ case '(':
+ case ')':
+ tokens.push([tag, tag]);
+ return 1;
+ }
+
+ return 0;
+};
+
+exports.tokenise = function(source) {
+ var i = 0;
+ while(chunk = source.slice(i)) {
+ var diff = identifierToken() || numberToken() || stringToken() || commentToken() || whitespaceToken() || lineToken() || literalToken();
+ if(!diff) {
+ throw "Couldn't tokenise: " + chunk.substring(0, chunk.indexOf("\n"));;
+ }
+ i += diff;
+ }
+
+ tokens.push(['EOF', '']);
+
+ return tokens;
+};
+
+if(!module.parent) {
+ exports.tokenise([
+ "// Testing",
+ " ",
+ "let x =",
+ " 8",
+ " if true then 10 else 100",
+ " if true then",
+ " false",
+ " else",
+ " true",
+ " // Inner comment",
+ " console.log 10 * 20",
+ " console.log [1, 2, 3].length",
+ " true",
+ "console.log 'example'"
+ ].join("\n"));
+ console.log(tokens);
+}
155 src/nodes.js
@@ -0,0 +1,155 @@
+exports.nodes = {
+ Arg: function(name, type) {
+ this.name = name;
+
+ // Optional
+ this.type = type;
+
+ this.accept = function(a) {
+ if(a.visitArg) {
+ return a.visitArg(this);
+ }
+ };
+ },
+ Function: function(name, args, body, type) {
+ this.name = name;
+ this.args = args;
+ this.body = body;
+
+ // Optional
+ this.type = type;
+
+ this.accept = function(a) {
+ if(a.visitFunction) {
+ return a.visitFunction(this);
+ }
+ };
+ },
+ Data: function(name) {
+ this.name = name;
+
+ this.accept = function(a) {
+ if(a.visitData) {
+ return a.visitData(this);
+ }
+ };
+ },
+ Let: function(name, value, type) {
+ this.name = name;
+ this.value = value;
+
+ // Optional
+ this.type = type;
+
+ this.accept = function(a) {
+ if(a.visitLet) {
+ return a.visitLet(this);
+ }
+ };
+ },
+ Call: function(func, args) {
+ this.func = func;
+ this.args = args;
+
+ this.accept = function(a) {
+ if(a.visitCall) {
+ return a.visitCall(this);
+ }
+ };
+ },
+ IfThenElse: function(condition, ifTrue, ifFalse) {
+ this.condition = condition;
+ this.ifTrue = ifTrue;
+ this.ifFalse = ifFalse;
+
+ this.accept = function(a) {
+ if(a.visitIfThenElse) {
+ return a.visitIfThenElse(this);
+ }
+ };
+ },
+ Comment: function(value) {
+ this.value = value;
+
+ this.accept = function(a) {
+ if(a.visitComment) {
+ return a.visitComment(this);
+ }
+ };
+ },
+ Access: function(value, property) {
+ this.value = value;
+ this.property = property;
+
+ this.accept = function(a) {
+ if(a.visitAccess) {
+ return a.visitAccess(this);
+ }
+ };
+ },
+ Operator: function(name, left, right) {
+ this.name = name;
+ this.left = left;
+ this.right = right;
+
+ this.accept = function(a) {
+ if(a.visitOperator) {
+ return a.visitOperator(this);
+ }
+ };
+ },
+ Identifier: function(value) {
+ this.value = value;
+
+ this.accept = function(a) {
+ if(a.visitIdentifier) {
+ return a.visitIdentifier(this);
+ }
+ };
+ },
+ Number: function(value) {
+ this.value = value;
+
+ this.accept = function(a) {
+ if(a.visitNumber) {
+ return a.visitNumber(this);
+ }
+ };
+ },
+ String: function(value) {
+ this.value = value;
+
+ this.accept = function(a) {
+ if(a.visitString) {
+ return a.visitString(this);
+ }
+ };
+ },
+ Boolean: function(value) {
+ this.value = value;
+
+ this.accept = function(a) {
+ if(a.visitBoolean) {
+ return a.visitBoolean(this);
+ }
+ };
+ },
+ Array: function(values) {
+ this.values = values;
+
+ this.accept = function(a) {
+ if(a.visitArray) {
+ return a.visitArray(this);
+ }
+ };
+ },
+ Object: function(values) {
+ this.values = values;
+
+ this.accept = function(a) {
+ if(a.visitObject) {
+ return a.visitObject(this);
+ }
+ };
+ }
+};
395 src/typeinference.js
@@ -0,0 +1,395 @@
+// ## Algorithm W (Damas-Hindley-Milner)
+//
+// This is based on Robert Smallshire's [Python code](http://bit.ly/bbVmmX).
+// Which is based on Andrew's [Scala code](http://bit.ly/aztXwD). Which is based
+// on Nikita Borisov's [Perl code](http://bit.ly/myq3uA). Which is based on Luca
+// Cardelli's [Modula-2 code](http://bit.ly/Hjpvb). Wow.
+
+// Type variable and built-in types are defined in the `types` module.
+var t = require('./types');
+
+// ### Unification
+//
+// This is the process of finding a type that satisfies some given constraints.
+// In this system, unification will try to satisfy that either:
+//
+// 1. `t1` and `t2` are equal type variables
+// 2. `t1` and `t2` are equal types
+//
+// In case #1, if `t1` is a type variable and `t2` is not currently equal,
+// unification will set `t1` to have an instance of `t2`. When `t1` is pruned,
+// it will unchain to a type without an instance.
+//
+// In case #2, do a deep unification on the type, using recursion.
+//
+// If neither constraint can be met, the process will throw an error message.
+var unify = function(t1, t2) {
+ var i;
+ t1 = prune(t1);
+ t2 = prune(t2);
+ if(t1 instanceof t.Variable) {
+ if(t1 != t2) {
+ if(occursInType(t1, t2)) {
+ throw "Recursive unification";
+ }
+ t1.instance = t2;
+ }
+ } else if(t1 instanceof t.BaseType && t2 instanceof t.Variable) {
+ unify(t2, t1);
+ } else if(t1 instanceof t.BaseType && t2 instanceof t.BaseType) {
+ if(t1.name != t2.name || t1.types.length != t2.types.length) {
+ throw new Error("Type error: " + t1.toString() + " is not " + t2.toString());
+ }
+ for(i = 0; i < Math.min(t1.types.length, t2.types.length); i++) {
+ unify(t1.types[i], t2.types[i]);
+ }
+ } else {
+ throw new Error("Not unified");
+ }
+};
+
+// ### Prune
+//
+// This will unchain variables until it gets to a type or variable without an
+// instance. See `unify` for some details about type variable instances.
+var prune = function(type) {
+ if(type instanceof t.Variable && type.instance) {
+ type.instance = prune(type.instance);
+ return type.instance;
+ }
+ return type;
+};
+
+// ### Fresh type
+//
+// Getting a "fresh" type will create a recursive copy. When a generic type
+// variable is encountered, a new variable is generated and substituted in.
+//
+// *Note*: Copied types are instantiated through the BaseType constructor, this
+// means `instanceof` can't be used for determining a subtype.
+//
+// A fresh type is only returned when an identifier is found during analysis.
+// See `analyse` for some context.
+var fresh = function(type, nonGeneric, mappings) {
+ if(!mappings) mappings = {};
+
+ type = prune(type);
+ if(type instanceof t.Variable) {
+ if(occursInTypeArray(type, nonGeneric)) {
+ return type;
+ } else {
+ if(!mappings[type.id]) {
+ mappings[type.id] = new t.Variable();
+ }
+ return mappings[type.id];
+ }
+ }
+
+ return new type.constructor(type.map(function(type) {
+ return fresh(type, nonGeneric, mappings);
+ }));
+};
+
+// ### Occurs check
+//
+// These functions check whether the type `t2` is equal to or contained within
+// the type `t1`. Used for checking recursive definitions in `unify` and
+// checking if a variable is non-generic in `fresh`.
+var occursInType = function(t1, t2) {
+ t2 = prune(t2);
+ if(t2 == t1) {
+ return true;
+ } else if(t2 instanceof t.BaseType) {
+ return occursInTypeArray(t1, t2.types);
+ }
+ return false;
+};
+
+var occursInTypeArray = function(t1, types) {
+ return types.map(function(t2) {
+ return occursInType(t1, t2);
+ }).indexOf(true) >= 0;
+};
+
+// ### Type analysis
+//
+// `analyse` is the core inference function. It takes an AST node and returns
+// the infered type.
+var analyse = function(node, env, nonGeneric) {
+ if(!nonGeneric) nonGeneric = [];
+
+ return node.accept({
+ // #### Function definition
+ //
+ // Assigns a type variable to each typeless argument and return type.
+ //
+ // Each typeless argument also gets added to the non-generic scope
+ // array. The `fresh` function can then return the existing type from
+ // the scope.
+ //
+ // Assigns the function's type in the environment and returns it.
+ //
+ // We create temporary types for recursive definitions.
+ visitFunction: function() {
+ var types = [];
+ var newNonGeneric = nonGeneric.slice();
+
+ var tempTypes = [];
+ for(var i = 0; i < node.args.length; i++) {
+ tempTypes.push(new t.Variable());
+ }
+ tempTypes.push(new t.Variable());
+ env[node.name] = new t.FunctionType(tempTypes);
+
+ node.args.forEach(function(arg, i) {
+ var argType;
+ if(arg.type) {
+ argType = nodeToType(arg.type);
+ } else {
+ argType = tempTypes[i];
+ newNonGeneric.push(argType);
+ }
+ env[arg.name] = argType;
+ types.push(argType);
+ });
+
+ var scopeTypes = node.body.map(function(expression) {
+ return analyse(expression, env, newNonGeneric);
+ });
+
+ var resultType = scopeTypes[scopeTypes.length - 1];
+ types.push(resultType);
+
+ var annotationType;
+ if(node.type) {
+ annotationType = nodeToType(node.type);
+ unify(resultType, annotationType);
+ }
+
+ var functionType = new t.FunctionType(types);
+ env[node.name] = functionType;
+
+ return functionType;
+ },
+ visitIfThenElse: function() {
+ var ifTrueScopeTypes = node.ifTrue.map(function(expression) {
+ return analyse(expression, env, nonGeneric);
+ });
+ var ifTrueType = ifTrueScopeTypes[ifTrueScopeTypes.length - 1];
+
+ var ifFalseScopeTypes = node.ifFalse.map(function(expression) {
+ return analyse(expression, env, nonGeneric);
+ });
+ var ifFalseType = ifFalseScopeTypes[ifFalseScopeTypes.length - 1];
+
+ unify(ifTrueType, ifFalseType);
+
+ return ifTrueType;
+ },
+ // #### Function call
+ //
+ // Ensures that all argument types `unify` with the defined function and
+ // returns the function's result type.
+ visitCall: function() {
+ var types = [];
+
+ node.args.forEach(function(arg) {
+ var argType = analyse(arg, env, nonGeneric);
+ types.push(argType);
+ });
+
+ var resultType = new t.Variable();
+ types.push(resultType);
+
+ var funType = analyse(node.func, env, nonGeneric);
+ unify(new t.FunctionType(types), funType);
+
+ return resultType;
+ },
+ // #### Let binding
+ //
+ // Infer the value's type, assigns it in the environment and returns it.
+ visitLet: function() {
+ var valueType = analyse(node.value, env, nonGeneric);
+
+ var annotionType;
+ if(node.type) {
+ annotionType = nodeToType(node.type);
+ unify(valueType, annotionType);
+ }
+
+ env[node.name] = valueType;
+
+ return valueType;
+ },
+ visitAccess: function() {
+ var valueType = analyse(node.value, env, nonGeneric);
+
+ unify(new t.ObjectType({}), valueType);
+
+ return valueType.getPropertyType(node.property);
+ },
+ visitOperator: function() {
+ var leftType = analyse(node.left, env, nonGeneric);
+ var rightType = analyse(node.right, env, nonGeneric);
+ unify(leftType, rightType);
+
+ return leftType;
+ },
+ // #### Identifier
+ //
+ // Creates a `fresh` copy of a type if the name is found in an
+ // environment, otherwise throws an error.
+ visitIdentifier: function() {
+ var name = node.value;
+ if(!env[name]) {
+ throw JSON.stringify(name) + " is not defined";
+ }
+ return fresh(env[name], nonGeneric);
+ },
+ // #### Primitive type
+ visitNumber: function() {
+ return new t.NumberType();
+ },
+ visitString: function() {
+ return new t.StringType();
+ },
+ visitBoolean: function() {
+ return new t.BooleanType();
+ },
+ visitArray: function() {
+ return new t.ArrayType();
+ },
+ visitObject: function() {
+ return new t.ObjectType({});
+ }
+ });
+};
+
+
+// Converts an AST node to type system type.
+var nodeToType = function(type) {
+ switch(type.value) {
+ case 'Number':
+ return new t.NumberType();
+ case 'String':
+ return new t.StringType();
+ case 'Boolean':
+ return new t.BooleanType();
+ default:
+ return type; // Shouldn't happen
+ }
+};
+
+// Run inference on an array of AST nodes.
+var typecheck = function(ast, builtins) {
+ if(!builtins) builtins = {};
+
+ return ast.map(function(node) {
+ return analyse(node, builtins);
+ });
+};
+
+exports.typecheck = typecheck;
+
+// ## Examples
+if(!module.parent) {
+ (function() {
+ var types = typecheck([
+ // let a = 10
+ //
+ // Result: Number
+ {
+ accept: function(a) {
+ return a.visitLet();
+ },
+ name: 'a',
+ value: {
+ accept: function(a) {
+ return a.visitNumber();
+ },
+ value: 10
+ }
+ },
+ // fun id x = x
+ //
+ // Result: Function('a,'a)
+ {
+ accept: function(a) {
+ return a.visitFunction();
+ },
+ name: "id",
+ args: [{name: "x"}],
+ body: [{
+ accept: function(a) {
+ return a.visitIdentifier();
+ },
+ value: "x"
+ }]
+ },
+ // fun explicitNumber (x : Number) = x
+ //
+ // Result: Function(Number,Number)
+ {
+ accept: function(a) {
+ return a.visitFunction();
+ },
+ name: "explicitNumber",
+ args: [
+ {
+ name: "x",
+ type: {
+ value: 'Number'
+ }
+ }
+ ],
+ body: [{
+ accept: function(a) {
+ return a.visitIdentifier();
+ },
+ value: "x"
+ }]
+ },
+ // fun ignoreArg a = 100
+ //
+ // Result: Function('b, Number)
+ {
+ accept: function(a) {
+ return a.visitFunction();
+ },
+ name: "ignoreArg",
+ args: [{name: "a"}],
+ body: [{
+ accept: function(a) {
+ return a.visitNumber();
+ },
+ value: 100
+ }]
+ },
+ // id 200
+ //
+ // Result: Number
+ {
+ accept: function(a) {
+ return a.visitCall();
+ },
+ name: {
+ accept: function(a) {
+ return a.visitIdentifier();
+ },
+ value: "id"
+ },
+ args: [
+ {
+ accept: function(a) {
+ return a.visitNumber();
+ },
+ value: 100
+ }
+ ]
+ }
+ ]);
+
+ console.log(types.toString());
+ })();
+}
100 src/types.js
@@ -0,0 +1,100 @@
+// ## Type variable
+//
+// A type variable represents an parameter with an unknown type or any
+// polymorphic type. For example:
+//
+// fun id x = x
+//
+// Here, `id` has the polymorphic type `'a -> 'a`.
+var Variable = function() {
+ this.id = Variable.nextId;
+ Variable.nextId++;
+ this.instance = null;
+};
+Variable.nextId = 0;
+exports.Variable = Variable;
+// Type variables should look like `'a`. If the variable has an instance, that
+// should be used for the string instead.
+Variable.prototype.toString = function() {
+ if(!this.instance) {
+ return "'" + String.fromCharCode("a".charCodeAt(0) + this.id);
+ }
+ return this.instance.toString();
+};
+
+// ## Base type
+//
+// Base type for all specific types. Using this type as the prototype allows the
+// use of `instanceof` to detect a type variable or an actual type.
+var BaseType = function() {
+ this.types = [];
+};
+BaseType.prototype.map = function() {};
+BaseType.prototype.toString = function() {
+ return this.name;
+};
+exports.BaseType = BaseType;
+
+// ## Specific types
+//
+// A `FunctionType` contains a `types` array. The last element represents the
+// return type. Each element before represents an argument type.
+var FunctionType = function(types) {
+ this.types = types;
+};
+FunctionType.prototype = new BaseType();
+FunctionType.prototype.constructor = FunctionType;
+FunctionType.prototype.name = "Function";
+FunctionType.prototype.map = function(f) {
+ return this.types.map(f);
+};
+FunctionType.prototype.toString = function() {
+ typeString = this.types.map(function(type) {
+ return type.toString();
+ }).toString();
+ return this.name + "(" + typeString + ")";
+};
+exports.FunctionType = FunctionType;
+
+var NumberType = function() {};
+NumberType.prototype = new BaseType();
+NumberType.prototype.constructor = NumberType;
+NumberType.prototype.name = "Number";
+exports.NumberType = NumberType;
+
+var StringType = function() {};
+StringType.prototype = new BaseType();
+StringType.prototype.constructor = StringType;
+StringType.prototype.name = "String";
+exports.StringType = StringType;
+
+var BooleanType = function() {};
+BooleanType.prototype = new BaseType();
+BooleanType.prototype.constructor = BooleanType;
+BooleanType.prototype.name = "Boolean";
+exports.BooleanType = BooleanType;
+
+var ArrayType = function() {};
+ArrayType.prototype = new BaseType();
+ArrayType.prototype.constructor = ArrayType;
+ArrayType.prototype.name = "Array";
+exports.ArrayType = ArrayType;
+
+var ObjectType = function(props) {
+ this.props = props;
+};
+ObjectType.prototype = new BaseType();
+ObjectType.prototype.constructor = ObjectType;
+ObjectType.prototype.name = "Object";
+ObjectType.prototype.map = function(f) {
+ var props = this.props;
+ var name;
+ for(name in props) {
+ props[name] = f(props[name]);
+ }
+ return props;
+};
+ObjectType.prototype.getPropertyType = function(prop) {
+ return this.props[prop];
+};
+exports.ObjectType = ObjectType;
Please sign in to comment.
Something went wrong with that request. Please try again.