Skip to content

Commit 3baec6a

Browse files
committed
Merge branch 'inferred-returns'
2 parents 92b51e7 + 9780372 commit 3baec6a

File tree

11 files changed

+170
-46
lines changed

11 files changed

+170
-46
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ language: node_js
22
node_js:
33
- "0.12"
44
before_script:
5-
- "npm run grammar2"
5+
- "npm run grammar"
66
- "npm run gen-spec"
77
script: "npm run test && npm run test-spec"
88
after_script:

examples/simple.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,16 @@ if b {
99
} else if a {
1010
console.log("2")
1111
}
12+
13+
var c = func () {
14+
if 1 {
15+
return 2
16+
} else {
17+
return
18+
}
19+
}
20+
console.log(c())
21+
22+
var d = func () { return }
23+
console.log(d)
1224

lib/ast.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ var Literal = function Literal (value, typeName) {
132132
}
133133
inherits(Literal, Node)
134134
Literal.prototype.print = function () { out.write(this.toString()) }
135-
Literal.prototype.toString = function () { return this.value.toString() }
135+
Literal.prototype.toString = function () { return JSON.stringify(this.value) }
136136

137137

138138
function Assignment (type, lvalue, op, rvalue) {
@@ -228,8 +228,12 @@ Function.prototype.print = function () {
228228
return ret
229229
}).join(', ')
230230
out.write('func ('+args+') ')
231+
var instance = this.type
231232
if (this.ret) {
232233
out.write('-> '+this.ret+' ')
234+
} else {
235+
// If we computed an inferred return type for the type
236+
out.write('-i> '+instance.type.ret.inspect()+' ')
233237
}
234238
this.block.print()
235239
}
@@ -297,11 +301,11 @@ Property.prototype.toString = function () {
297301
}
298302

299303

300-
function If (cond, block, elseIfs, els) {
301-
this.cond = cond
302-
this.block = block
303-
this.elseIfs = elseIfs ? elseIfs : null
304-
this.els = els ? els : null
304+
function If (cond, block, elseIfs, elseBlock) {
305+
this.cond = cond
306+
this.block = block
307+
this.elseIfs = elseIfs ? elseIfs : null
308+
this.elseBlock = elseBlock ? elseBlock : null
305309
}
306310
inherits(If, Node)
307311
If.prototype.print = function () {
@@ -316,6 +320,10 @@ If.prototype.print = function () {
316320
ei.block.print()
317321
}
318322
}
323+
if (this.elseBlock) {
324+
out.write(" else ")
325+
this.elseBlock.print()
326+
}
319327
}
320328

321329

@@ -371,7 +379,12 @@ function Return (expr) {
371379
}
372380
inherits(Return, Node)
373381
Return.prototype.print = function () { out.write(this.toString()) }
374-
Return.prototype.toString = function () { return 'return '+this.expr.toString() }
382+
Return.prototype.toString = function () {
383+
if (this.expr) {
384+
return 'return '+this.expr.toString()
385+
}
386+
return 'return'
387+
}
375388

376389

377390
function Root (statements) {
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
p.parseClass = function (name, block) { return [name, block] }
2222
p.parseInit = function (args, block) { return [args, block] }
2323
p.parseBlock = function (statements) { return statements }
24-
p.parseIf = function (cond, block, elseIfs) { return [cond, block, elseIfs] }
24+
p.parseIf = function (cond, block, elseIfs, elseBlock) { return [cond, block, elseIfs, elseBlock] }
2525
p.parseRoot = function (statements) { return statements }
2626
p.parseBinary = function (left, op, right) { return [left, op, right] }
2727
p.parseInteger = function (integerString) { return integerString }
@@ -71,12 +71,14 @@ ctrl = ifctrl
7171
/ forctrl
7272
/ returnctrl
7373

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

8183
whilectrl = "while" _ c:innerstmt _ b:block { return p.parseWhile(c, b) }
8284
forctrl = "for" _ i:innerstmt? _ ";" _ c:innerstmt? _ ";" _ a:innerstmt? _ b:block { return p.parseFor(i, c, a, b) }

lib/parser-extension.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ module.exports = function (p) {
3939
return init
4040
}
4141

42-
p.parseIf = function (cond, block, elseIfs) {
43-
return new AST.If(cond, block, elseIfs)
42+
p.parseIf = function (cond, block, elseIfs, elseBlock) {
43+
return new AST.If(cond, block, elseIfs, elseBlock)
4444
}
4545

4646
p.parseRoot = function (statements) {

lib/parser.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
var fs = require('fs')
22

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

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

27-
var grammar2 = require('./grammar2'),
27+
var grammar = require('./grammar'),
2828
AST = require('./ast'),
2929
types = require('./types')
3030
stderr = process.stderr,
@@ -37,7 +37,7 @@ var Parser = function () {
3737
}
3838
Parser.prototype.parse = function (code) {
3939
var tree
40-
tree = grammar2.parse(code, {file: this.file})
40+
tree = grammar.parse(code, {file: this.file})
4141
return tree
4242
}
4343

lib/types.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Type.prototype.toString = function () {
2323
if (this.inspect) { return this.inspect() }
2424
return this.constructor.name
2525
}
26+
Type.prototype.inspect = function () { return this.constructor.name }
2627

2728

2829
// Instance of a type (ie. all variables are Instances)
@@ -72,8 +73,19 @@ function Any () {
7273
}
7374
inherits(Any, Type)
7475
// Any always equals another type
75-
Any.prototype.equals = function (other) { return true }
76-
Any.prototype.inspect = function () { return 'Any' }
76+
Any.prototype.equals = function (other) { return true }
77+
78+
79+
function Void () {
80+
_super(this).call(this, 'fake')
81+
this.intrinsic = true
82+
this.supertype = null
83+
}
84+
inherits(Void, Type)
85+
Void.prototype.equals = function (other) {
86+
// There should never be more than 1 instance of Void
87+
return this === other
88+
}
7789

7890

7991
function String (supertype) {
@@ -136,8 +148,8 @@ function Function (supertype, args, ret) {
136148
}
137149
inherits(Function, Type)
138150
Function.prototype.inspect = function () {
139-
var ret = (this.ret ? this.ret.inspect() : 'Void')
140-
var args = ''
151+
var ret = this.ret.inspect(),
152+
args = ''
141153
if (this.args.length > 0) {
142154
args = this.args.map(function (arg) { return arg.inspect() }).join(', ')
143155
}
@@ -190,6 +202,7 @@ module.exports = {
190202
Type: Type,
191203
Instance: Instance,
192204
Any: Any,
205+
Void: Void,
193206
Object: Object,
194207
String: String,
195208
Number: Number,

lib/typesystem.js

Lines changed: 80 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ TypeSystem.prototype.visitIf = function (node, scope) {
227227

228228
this.visitExpression(node.cond, scope)
229229

230+
// Handle the main if block
230231
var blockScope = new Scope(scope)
231232
this.visitBlock(node.block, blockScope)
232233

@@ -239,6 +240,11 @@ TypeSystem.prototype.visitIf = function (node, scope) {
239240
this.visitBlock(elseIf.block, elseIfBlockScope)
240241
}
241242
}
243+
// Handle the else block if present
244+
if (node.elseBlock) {
245+
var elseBlockScope = new Scope(scope)
246+
this.visitBlock(node.elseBlock, elseBlockScope)
247+
}
242248
}
243249

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

253259
TypeSystem.prototype.visitReturn = function (node, scope, parentNode) {
254-
if (node.expr === null || node.expr === undefined) {
255-
throw new TypeError('Cannot handle empty Return')
260+
if (node.expr === undefined) {
261+
throw new TypeError('Cannot handle undefined expression in Return')
262+
}
263+
var exprType = null
264+
if (node.expr === null) {
265+
var voidType = this.root.getLocal('Void')
266+
exprType = new types.Instance(voidType)
267+
} else {
268+
var expr = node.expr
269+
exprType = this.resolveExpression(expr, scope)
256270
}
257-
var expr = node.expr
258-
var exprType = this.resolveExpression(expr, scope)
259271
node.type = exprType
272+
// Handle the parent block if present
260273
if (parentNode) {
261274
if (!((parentNode instanceof AST.Block) || (parentNode instanceof AST.Root))) {
262275
throw new TypeError('Expected Block or Root as parent of Return', node)
@@ -267,7 +280,7 @@ TypeSystem.prototype.visitReturn = function (node, scope, parentNode) {
267280
}
268281
// The expression should return an instance, we'll have to unbox that
269282
assertInstanceOf(exprType, types.Instance, 'Expected Instance as argument to Return')
270-
parentNode.returnType = exprType.type
283+
parentNode.returnType = exprType ? exprType.type : null
271284
}
272285
}
273286

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

466479
block.statements.forEach(function (stmt) {
480+
var types = null
467481
switch (stmt.constructor) {
468482
case AST.If:
483+
types = getAllReturnTypes(stmt.block)
484+
if (stmt.elseBlock) {
485+
types = types.concat(getAllReturnTypes(stmt.elseBlock))
486+
}
487+
returnTypes = returnTypes.concat(types)
488+
break
469489
case AST.While:
470490
case AST.For:
471-
var subblockTypes = getAllReturnTypes(stmt.block)
472-
returnTypes = returnTypes.concat(subblockTypes)
491+
types = getAllReturnTypes(stmt.block)
492+
returnTypes = returnTypes.concat(types)
493+
break
473494
}
474495
})
475496
return returnTypes
@@ -479,6 +500,8 @@ TypeSystem.prototype.visitFunction = function (node, parentScope, immediate) {
479500
if (node.type) { return node.type }
480501
var self = this
481502
var type = new types.Function(this.rootObject)
503+
// Set the type of this node to an instance of the function type
504+
node.type = new types.Instance(type)
482505

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

536+
// Get all possible return types of this function (recursively collects
537+
// returning child blocks).
538+
var returnTypes = getAllReturnTypes(node.block)
539+
540+
// If there is a declared return type then we need to check that all the found
541+
// returns match that type
513542
if (type.ret) {
514-
// Get all possible return types of this function (recursively collects
515-
// returning child blocks).
516-
var returnTypes = getAllReturnTypes(node.block)
517543
returnTypes.forEach(function (returnType) {
518544
if (!type.ret.equals(returnType)) {
519545
throw new TypeError('Type returned by function does not match declared return type')
520546
}
521547
})
522-
// Box the type and return
523-
node.type = new types.Instance(type)
524548
return
525549
}
526-
throw new TypeError('Inferred return types not supported yet', node)
527550

528-
// Then we'll find all the `return`s and get their types
529-
var returns = []
530-
// TODO: Actually visit and resolve return types
551+
// Otherwise we need to try to unify the returns; this could potentially be
552+
// a very expensive operation, so we'll warn the user if they do too many
553+
if (returnTypes.length > 4) {
554+
var returns = returnTypes.length,
555+
file = node._file,
556+
line = node._line,
557+
warning = "Warning: Encountered "+returns+" return statements in function\n"+
558+
" Computing type unions can be expensive and should be used carefully!\n"+
559+
" at "+file+":"+line+"\n"
560+
process.stderr.write(warning)
561+
}
562+
// Slow quadratic uniqueness checking to reduce the set of return types
563+
// to distinct ones
564+
var reducedTypes = uniqueWithComparator(returnTypes, function (a, b) {
565+
return a.equals(b)
566+
})
567+
if (reducedTypes.length > 1) {
568+
var t = reducedTypes.map(function (t) { return t.inspect() }).join(', ')
569+
throw new TypeError('Too many return types (have '+t+')', node)
570+
}
571+
// Final return type
572+
var returnType = null
573+
if (reducedTypes.length !== 0) {
574+
returnType = reducedTypes[0]
575+
}
576+
// Update the type definition (if there we 0 then it will be null which is
577+
// Void in the type-system)
578+
type.ret = returnType
579+
}
580+
581+
function uniqueWithComparator (array, comparator) {
582+
var acc = [],
583+
length = array.length
584+
for (var i = 0; i < length; i++) {
585+
for (var j = i + 1; j < length; j++) {
586+
var a = array[i],
587+
b = array[j]
588+
if (comparator(a, b)) { j = ++i }
589+
}
590+
acc.push(array[i])
591+
}
592+
return acc
531593
}
532594

533595

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

666729

667730
module.exports = {TypeSystem: TypeSystem}
668-

0 commit comments

Comments
 (0)