Skip to content

Commit

Permalink
Merge branch 'inferred-returns'
Browse files Browse the repository at this point in the history
  • Loading branch information
dirk committed Apr 2, 2015
2 parents 92b51e7 + 9780372 commit 3baec6a
Show file tree
Hide file tree
Showing 11 changed files with 170 additions and 46 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ language: node_js
node_js:
- "0.12"
before_script:
- "npm run grammar2"
- "npm run grammar"
- "npm run gen-spec"
script: "npm run test && npm run test-spec"
after_script:
Expand Down
12 changes: 12 additions & 0 deletions examples/simple.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,16 @@ if b {
} else if a {
console.log("2")
}

var c = func () {
if 1 {
return 2
} else {
return
}
}
console.log(c())

var d = func () { return }
console.log(d)

27 changes: 20 additions & 7 deletions lib/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ var Literal = function Literal (value, typeName) {
}
inherits(Literal, Node)
Literal.prototype.print = function () { out.write(this.toString()) }
Literal.prototype.toString = function () { return this.value.toString() }
Literal.prototype.toString = function () { return JSON.stringify(this.value) }


function Assignment (type, lvalue, op, rvalue) {
Expand Down Expand Up @@ -228,8 +228,12 @@ Function.prototype.print = function () {
return ret
}).join(', ')
out.write('func ('+args+') ')
var instance = this.type
if (this.ret) {
out.write('-> '+this.ret+' ')
} else {
// If we computed an inferred return type for the type
out.write('-i> '+instance.type.ret.inspect()+' ')
}
this.block.print()
}
Expand Down Expand Up @@ -297,11 +301,11 @@ Property.prototype.toString = function () {
}


function If (cond, block, elseIfs, els) {
this.cond = cond
this.block = block
this.elseIfs = elseIfs ? elseIfs : null
this.els = els ? els : null
function If (cond, block, elseIfs, elseBlock) {
this.cond = cond
this.block = block
this.elseIfs = elseIfs ? elseIfs : null
this.elseBlock = elseBlock ? elseBlock : null
}
inherits(If, Node)
If.prototype.print = function () {
Expand All @@ -316,6 +320,10 @@ If.prototype.print = function () {
ei.block.print()
}
}
if (this.elseBlock) {
out.write(" else ")
this.elseBlock.print()
}
}


Expand Down Expand Up @@ -371,7 +379,12 @@ function Return (expr) {
}
inherits(Return, Node)
Return.prototype.print = function () { out.write(this.toString()) }
Return.prototype.toString = function () { return 'return '+this.expr.toString() }
Return.prototype.toString = function () {
if (this.expr) {
return 'return '+this.expr.toString()
}
return 'return'
}


function Root (statements) {
Expand Down
8 changes: 5 additions & 3 deletions lib/grammar2.pegjs → lib/grammar.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
p.parseClass = function (name, block) { return [name, block] }
p.parseInit = function (args, block) { return [args, block] }
p.parseBlock = function (statements) { return statements }
p.parseIf = function (cond, block, elseIfs) { return [cond, block, elseIfs] }
p.parseIf = function (cond, block, elseIfs, elseBlock) { return [cond, block, elseIfs, elseBlock] }
p.parseRoot = function (statements) { return statements }
p.parseBinary = function (left, op, right) { return [left, op, right] }
p.parseInteger = function (integerString) { return integerString }
Expand Down Expand Up @@ -71,12 +71,14 @@ ctrl = ifctrl
/ forctrl
/ returnctrl

ifctrl = "if" _ c:innerstmt __ b:block ei:(__ elseifcont)* {
ifctrl = "if" _ c:innerstmt __ b:block ei:(__ elseifcont)* e:(__ elsecont)? {
ei = ei.map(function (pair) { return pair[1] })
return p.parseIf(c, b, ei)
e = e ? e[1] : null
return p.parseIf(c, b, ei, e)
}
// Continuations of the if control with else-ifs
elseifcont = "else" __ "if" _ c:innerstmt __ b:block { return p.parseIf(c, b, null) }
elsecont = "else" __ b:block { return b }

whilectrl = "while" _ c:innerstmt _ b:block { return p.parseWhile(c, b) }
forctrl = "for" _ i:innerstmt? _ ";" _ c:innerstmt? _ ";" _ a:innerstmt? _ b:block { return p.parseFor(i, c, a, b) }
Expand Down
4 changes: 2 additions & 2 deletions lib/parser-extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ module.exports = function (p) {
return init
}

p.parseIf = function (cond, block, elseIfs) {
return new AST.If(cond, block, elseIfs)
p.parseIf = function (cond, block, elseIfs, elseBlock) {
return new AST.If(cond, block, elseIfs, elseBlock)
}

p.parseRoot = function (statements) {
Expand Down
12 changes: 6 additions & 6 deletions lib/parser.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
var fs = require('fs')

// Before loading the parser let's check to make sure it's up-to-date
var grammarFile = __dirname+'/grammar2.js',
grammarSourceFile = __dirname+'/grammar2.pegjs',
var grammarFile = __dirname+'/grammar.js',
grammarSourceFile = __dirname+'/grammar.pegjs',
grammarStat = null,
grammarSourceStat = null

Expand All @@ -11,7 +11,7 @@ try {
grammarStat = fs.statSync(grammarFile)
} catch (err) {
if (err.code === 'ENOENT') {
process.stderr.write("Missing generated parser file, please run `npm run grammar2` to generate it.\n")
process.stderr.write("Missing generated parser file, please run `npm run grammar` to generate it.\n")
process.exit(1)
}
// Don't recognize this error, rethrow
Expand All @@ -20,11 +20,11 @@ try {
// Now check to make sure that it's up-to-date
grammarSourceStat = fs.statSync(grammarSourceFile)
if (grammarSourceStat.mtime > grammarStat.mtime) {
process.stderr.write("Parser file is out of date, please do `npm run grammar2` to re-generate it.\n")
process.stderr.write("Parser file is out of date, please do `npm run grammar` to re-generate it.\n")
process.exit(1)
}

var grammar2 = require('./grammar2'),
var grammar = require('./grammar'),
AST = require('./ast'),
types = require('./types')
stderr = process.stderr,
Expand All @@ -37,7 +37,7 @@ var Parser = function () {
}
Parser.prototype.parse = function (code) {
var tree
tree = grammar2.parse(code, {file: this.file})
tree = grammar.parse(code, {file: this.file})
return tree
}

Expand Down
21 changes: 17 additions & 4 deletions lib/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Type.prototype.toString = function () {
if (this.inspect) { return this.inspect() }
return this.constructor.name
}
Type.prototype.inspect = function () { return this.constructor.name }


// Instance of a type (ie. all variables are Instances)
Expand Down Expand Up @@ -72,8 +73,19 @@ function Any () {
}
inherits(Any, Type)
// Any always equals another type
Any.prototype.equals = function (other) { return true }
Any.prototype.inspect = function () { return 'Any' }
Any.prototype.equals = function (other) { return true }


function Void () {
_super(this).call(this, 'fake')
this.intrinsic = true
this.supertype = null
}
inherits(Void, Type)
Void.prototype.equals = function (other) {
// There should never be more than 1 instance of Void
return this === other
}


function String (supertype) {
Expand Down Expand Up @@ -136,8 +148,8 @@ function Function (supertype, args, ret) {
}
inherits(Function, Type)
Function.prototype.inspect = function () {
var ret = (this.ret ? this.ret.inspect() : 'Void')
var args = ''
var ret = this.ret.inspect(),
args = ''
if (this.args.length > 0) {
args = this.args.map(function (arg) { return arg.inspect() }).join(', ')
}
Expand Down Expand Up @@ -190,6 +202,7 @@ module.exports = {
Type: Type,
Instance: Instance,
Any: Any,
Void: Void,
Object: Object,
String: String,
Number: Number,
Expand Down
98 changes: 80 additions & 18 deletions lib/typesystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ TypeSystem.prototype.visitIf = function (node, scope) {

this.visitExpression(node.cond, scope)

// Handle the main if block
var blockScope = new Scope(scope)
this.visitBlock(node.block, blockScope)

Expand All @@ -239,6 +240,11 @@ TypeSystem.prototype.visitIf = function (node, scope) {
this.visitBlock(elseIf.block, elseIfBlockScope)
}
}
// Handle the else block if present
if (node.elseBlock) {
var elseBlockScope = new Scope(scope)
this.visitBlock(node.elseBlock, elseBlockScope)
}
}

TypeSystem.prototype.visitWhile = function (node, scope) {
Expand All @@ -251,12 +257,19 @@ TypeSystem.prototype.visitWhile = function (node, scope) {
}

TypeSystem.prototype.visitReturn = function (node, scope, parentNode) {
if (node.expr === null || node.expr === undefined) {
throw new TypeError('Cannot handle empty Return')
if (node.expr === undefined) {
throw new TypeError('Cannot handle undefined expression in Return')
}
var exprType = null
if (node.expr === null) {
var voidType = this.root.getLocal('Void')
exprType = new types.Instance(voidType)
} else {
var expr = node.expr
exprType = this.resolveExpression(expr, scope)
}
var expr = node.expr
var exprType = this.resolveExpression(expr, scope)
node.type = exprType
// Handle the parent block if present
if (parentNode) {
if (!((parentNode instanceof AST.Block) || (parentNode instanceof AST.Root))) {
throw new TypeError('Expected Block or Root as parent of Return', node)
Expand All @@ -267,7 +280,7 @@ TypeSystem.prototype.visitReturn = function (node, scope, parentNode) {
}
// The expression should return an instance, we'll have to unbox that
assertInstanceOf(exprType, types.Instance, 'Expected Instance as argument to Return')
parentNode.returnType = exprType.type
parentNode.returnType = exprType ? exprType.type : null
}
}

Expand Down Expand Up @@ -464,12 +477,20 @@ function getAllReturnTypes (block) {
if (block.returnType) { returnTypes.push(block.returnType) }

block.statements.forEach(function (stmt) {
var types = null
switch (stmt.constructor) {
case AST.If:
types = getAllReturnTypes(stmt.block)
if (stmt.elseBlock) {
types = types.concat(getAllReturnTypes(stmt.elseBlock))
}
returnTypes = returnTypes.concat(types)
break
case AST.While:
case AST.For:
var subblockTypes = getAllReturnTypes(stmt.block)
returnTypes = returnTypes.concat(subblockTypes)
types = getAllReturnTypes(stmt.block)
returnTypes = returnTypes.concat(types)
break
}
})
return returnTypes
Expand All @@ -479,6 +500,8 @@ TypeSystem.prototype.visitFunction = function (node, parentScope, immediate) {
if (node.type) { return node.type }
var self = this
var type = new types.Function(this.rootObject)
// Set the type of this node to an instance of the function type
node.type = new types.Instance(type)

if (node.ret) {
type.ret = this.resolveType(node.ret)
Expand Down Expand Up @@ -510,24 +533,63 @@ TypeSystem.prototype.visitFunction = function (node, parentScope, immediate) {
// Begin by visiting our block
this.visitBlock(node.block, functionScope)

// Get all possible return types of this function (recursively collects
// returning child blocks).
var returnTypes = getAllReturnTypes(node.block)

// If there is a declared return type then we need to check that all the found
// returns match that type
if (type.ret) {
// Get all possible return types of this function (recursively collects
// returning child blocks).
var returnTypes = getAllReturnTypes(node.block)
returnTypes.forEach(function (returnType) {
if (!type.ret.equals(returnType)) {
throw new TypeError('Type returned by function does not match declared return type')
}
})
// Box the type and return
node.type = new types.Instance(type)
return
}
throw new TypeError('Inferred return types not supported yet', node)

// Then we'll find all the `return`s and get their types
var returns = []
// TODO: Actually visit and resolve return types
// Otherwise we need to try to unify the returns; this could potentially be
// a very expensive operation, so we'll warn the user if they do too many
if (returnTypes.length > 4) {
var returns = returnTypes.length,
file = node._file,
line = node._line,
warning = "Warning: Encountered "+returns+" return statements in function\n"+
" Computing type unions can be expensive and should be used carefully!\n"+
" at "+file+":"+line+"\n"
process.stderr.write(warning)
}
// Slow quadratic uniqueness checking to reduce the set of return types
// to distinct ones
var reducedTypes = uniqueWithComparator(returnTypes, function (a, b) {
return a.equals(b)
})
if (reducedTypes.length > 1) {
var t = reducedTypes.map(function (t) { return t.inspect() }).join(', ')
throw new TypeError('Too many return types (have '+t+')', node)
}
// Final return type
var returnType = null
if (reducedTypes.length !== 0) {
returnType = reducedTypes[0]
}
// Update the type definition (if there we 0 then it will be null which is
// Void in the type-system)
type.ret = returnType
}

function uniqueWithComparator (array, comparator) {
var acc = [],
length = array.length
for (var i = 0; i < length; i++) {
for (var j = i + 1; j < length; j++) {
var a = array[i],
b = array[j]
if (comparator(a, b)) { j = ++i }
}
acc.push(array[i])
}
return acc
}


Expand Down Expand Up @@ -609,7 +671,8 @@ TypeSystem.prototype.visitChain = function (node, scope) {
// Get the Instance type of the passing argument node
var itemArgInstance = itemArg.type
// Verify that the passed argument's type is an Instance box
assertInstanceOf(itemArgInstance, types.Instance, 'Expected Instance as function argument')
var failureMessage = 'Expected Instance as function argument, got: '+itemArgInstance.inspect()
assertInstanceOf(itemArgInstance, types.Instance, failureMessage)
// Unbox the instance
var itemArgType = itemArgInstance.type
// Then get the type from the function definition to compare to the
Expand Down Expand Up @@ -665,4 +728,3 @@ TypeSystem.prototype.visitMulti = function (node, scope) {


module.exports = {TypeSystem: TypeSystem}

Loading

0 comments on commit 3baec6a

Please sign in to comment.