2013-10-28 20:00:20 +00:00
// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
// Scratch HTML5 Player
// Interpreter.js
// Tim Mickel, July 2011
// Based on the original by John Maloney
'use strict' ;
var Block = function ( opAndArgs , optionalSubstack ) {
this . op = opAndArgs [ 0 ] ;
2013-11-01 22:44:51 -04:00
this . primFcn = interp . lookupPrim ( this . op ) ;
2013-10-28 20:00:20 +00:00
this . args = opAndArgs . slice ( 1 ) ; // arguments can be either or constants (numbers, boolean strings, etc.) or expressions (Blocks)
this . isLoop = false ; // set to true for loop blocks the first time they run
this . substack = optionalSubstack ;
this . subStack2 = null ;
this . nextBlock = null ;
this . tmp = - 1 ;
interp . fixArgs ( this ) ;
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
var Thread = function ( block , target ) {
this . nextBlock = block ; // next block to run; null when thread is finished
this . firstBlock = block ;
this . stack = [ ] ; // stack of enclosing control structure blocks
this . target = target ; // target object running the thread
this . tmp = null ; // used for thread operations like Timer
this . tmpObj = [ ] ; // used for Sprite operations like glide
this . firstTime = true ;
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
var Interpreter = function ( ) {
// Interpreter state
this . primitiveTable = { }
this . variables = { } ;
this . threads = [ ] ;
this . activeThread = new Thread ( null ) ;
this . WorkTime = 30 ;
this . currentMSecs = null ;
this . timer = new Timer ( ) ;
this . yield = false ;
this . doRedraw = false ;
this . opCount = 0 ; // used to benchmark the interpreter
2013-11-04 06:11:05 +00:00
this . debugOps = false ;
this . debugFunc = null ;
this . opCount2 = 0 ;
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
// Utilities for building blocks and sequences of blocks
2013-11-01 22:44:51 -04:00
Interpreter . prototype . fixArgs = function ( b ) {
2013-10-28 20:00:20 +00:00
// Convert the arguments of the given block into blocks or substacks if necessary.
// A block argument can be a constant (numbers, boolean strings, etc.), an expression (Blocks), or a substack (an array of blocks).
var newArgs = [ ] ;
for ( var i = 0 ; i < b . args . length ; i ++ ) {
var arg = b . args [ i ] ;
if ( arg && arg . constructor == Array ) {
if ( ( arg . length > 0 ) && ( arg [ 0 ] . constructor == Array ) ) {
// if first element arg is itself an array, then arg is a substack
2013-11-01 22:44:51 -04:00
if ( ! b . substack ) {
2013-10-28 20:00:20 +00:00
b . substack = this . makeBlockList ( arg ) ;
} else {
b . substack2 = this . makeBlockList ( arg ) ;
}
} else {
// arg is a block
newArgs . push ( new Block ( arg ) ) ;
}
} else {
newArgs . push ( arg ) ; // arg is a constant
}
}
b . args = newArgs ;
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
Interpreter . prototype . makeBlockList = function ( blockList ) {
var firstBlock = null , lastBlock = null ;
for ( var i = 0 ; i < blockList . length ; i ++ ) {
var b = new Block ( blockList [ i ] ) ;
if ( firstBlock == null ) firstBlock = b ;
if ( lastBlock ) lastBlock . nextBlock = b ;
lastBlock = b ;
}
return firstBlock ;
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
// The Interpreter proper
Interpreter . prototype . stepThreads = function ( ) {
var startTime ;
startTime = this . currentMSecs = this . timer . time ( ) ;
this . doRedraw = false ;
if ( this . threads . length == 0 ) return ;
while ( ( this . currentMSecs - startTime ) < this . WorkTime && ! this . doRedraw ) {
var threadStopped = false ;
for ( var a = this . threads . length - 1 ; a >= 0 ; -- a ) {
this . activeThread = this . threads [ a ] ;
this . stepActiveThread ( ) ;
if ( ! this . activeThread || this . activeThread . nextBlock == null ) {
threadStopped = true ;
}
}
if ( threadStopped ) {
var newThreads = [ ] ;
for ( var a = this . threads . length - 1 ; a >= 0 ; -- a ) {
if ( this . threads [ a ] . nextBlock != null ) {
newThreads . push ( this . threads [ a ] ) ;
}
2013-11-01 22:44:51 -04:00
}
2013-10-28 20:00:20 +00:00
this . threads = newThreads ;
if ( this . threads . length == 0 ) return ;
}
this . currentMSecs = this . timer . time ( ) ;
}
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
Interpreter . prototype . stepActiveThread = function ( ) {
// Run the active thread until it yields.
2013-11-01 22:44:51 -04:00
if ( typeof ( this . activeThread ) == 'undefined' ) {
2013-10-28 20:00:20 +00:00
return ;
}
var b = this . activeThread . nextBlock ;
if ( b == null ) return ;
this . yield = false ;
while ( true ) {
this . opCount ++ ;
// Advance the "program counter" to the next block before running the primitive.
// Control flow primitives (e.g. if) may change activeThread.nextBlock.
this . activeThread . nextBlock = b . nextBlock ;
2013-11-04 06:11:05 +00:00
if ( this . debugOps && this . debugFunc ) {
var finalArgs = new Array ( b . args . length ) ;
for ( var i = 0 ; i < b . args . length ; ++ i )
finalArgs [ i ] = this . arg ( b , i ) ;
this . debugFunc ( this . opCount2 , b . op , finalArgs ) ;
++ this . opCount2 ;
}
2013-10-28 20:00:20 +00:00
b . primFcn ( b ) ;
if ( this . yield ) { this . activeThread . nextBlock = b ; return ; }
b = this . activeThread . nextBlock ; // refresh local variable b in case primitive did some control flow
while ( b == null ) {
// end of a substack; pop the owning control flow block from stack
2013-11-01 22:44:51 -04:00
// Note: This is a loop to handle nested control flow blocks.
2013-10-28 20:00:20 +00:00
b = this . activeThread . stack . pop ( ) ;
if ( ( b == null ) || ( b . isLoop ) ) {
this . activeThread . nextBlock = b ;
return ; // yield at the end of a loop or when stack is empty
}
}
}
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
Interpreter . prototype . toggleThread = function ( b , targetObj ) {
var newThreads = [ ] , wasRunning = false ;
for ( var i = 0 ; i < this . threads . length ; i ++ ) {
if ( this . threads [ i ] . stack [ 0 ] == b ) {
wasRunning = true ;
} else {
newThreads . push ( this . threads [ i ] ) ;
}
}
this . threads = newThreads ;
2013-11-01 22:44:51 -04:00
if ( ! wasRunning ) {
2013-10-28 20:00:20 +00:00
this . startThread ( b , targetObj ) ;
}
2013-11-01 22:44:51 -04:00
}
2013-10-28 20:00:20 +00:00
Interpreter . prototype . startThread = function ( b , targetObj ) {
this . activeThread = new Thread ( b , targetObj ) ;
this . threads . push ( this . activeThread ) ;
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
Interpreter . prototype . restartThread = function ( b , targetObj ) {
// used by broadcast; stop any thread running on b, then start a new thread on b
var newThread = new Thread ( b , targetObj ) ;
var wasRunning = false ;
for ( var i = 0 ; i < this . threads . length ; i ++ ) {
if ( this . threads [ i ] . stack [ 0 ] == b ) {
this . threads [ i ] = newThread ;
wasRunning = true ;
}
}
if ( ! wasRunning ) {
this . threads . push ( newThread ) ;
}
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
Interpreter . prototype . arg = function ( block , index ) {
var arg = block . args [ index ] ;
if ( ( typeof ( arg ) == 'object' ) && ( arg . constructor == Block ) ) {
this . opCount ++ ;
return arg . primFcn ( arg ) ; // expression
}
return arg ;
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
Interpreter . prototype . targetSprite = function ( ) {
return this . activeThread . target ;
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
// Timer
Interpreter . prototype . startTimer = function ( secs ) {
var waitMSecs = 1000 * secs ;
if ( waitMSecs < 0 ) waitMSecs = 0 ;
this . activeThread . tmp = this . currentMSecs + waitMSecs ; // end time in milliseconds
this . activeThread . firstTime = false ;
this . yield = true ;
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
Interpreter . prototype . checkTimer = function ( ) {
// check for timer expiration and clean up if expired. return true when expired
if ( this . currentMSecs >= this . activeThread . tmp ) {
// time expired
this . activeThread . tmp = 0 ;
this . activeThread . firstTime = true ;
return true ;
} else {
this . yield = true ;
return false ;
}
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
Interpreter . prototype . redraw = function ( ) {
this . doRedraw = true ;
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
// Primitive operations
Interpreter . prototype . initPrims = function ( ) {
this . primitiveTable = { } ;
this . primitiveTable [ 'whenGreenFlag' ] = this . primNoop ;
this . primitiveTable [ 'whenKeyPressed' ] = this . primNoop ;
this . primitiveTable [ 'whenClicked' ] = this . primNoop ;
2013-11-01 22:44:51 -04:00
this . primitiveTable [ 'if' ] = function ( b ) { if ( interp . arg ( b , 0 ) ) interp . startSubstack ( b ) ; } ;
this . primitiveTable [ 'doForever' ] = function ( b ) { interp . startSubstack ( b , true ) ; } ;
2013-10-28 20:00:20 +00:00
this . primitiveTable [ 'doForeverIf' ] = function ( b ) { if ( interp . arg ( b , 0 ) ) interp . startSubstack ( b , true ) ; else interp . yield = true ; } ;
this . primitiveTable [ 'doIf' ] = function ( b ) { if ( interp . arg ( b , 0 ) ) interp . startSubstack ( b ) ; } ;
this . primitiveTable [ 'doRepeat' ] = this . primRepeat ;
this . primitiveTable [ 'doIfElse' ] = function ( b ) { if ( interp . arg ( b , 0 ) ) interp . startSubstack ( b ) ; else interp . startSubstack ( b , false , true ) ; } ;
2013-11-01 22:44:51 -04:00
this . primitiveTable [ 'doWaitUntil' ] = function ( b ) { if ( ! interp . arg ( b , 0 ) ) interp . yield = true ; } ;
this . primitiveTable [ 'doUntil' ] = function ( b ) { if ( ! interp . arg ( b , 0 ) ) interp . startSubstack ( b , true ) ; } ;
2013-10-28 20:00:20 +00:00
this . primitiveTable [ 'doReturn' ] = function ( b ) { interp . activeThread = new Thread ( null ) ; } ;
this . primitiveTable [ 'stopAll' ] = function ( b ) { interp . activeThread = new Thread ( null ) ; interp . threads = [ ] ; }
this . primitiveTable [ 'whenIReceive' ] = this . primNoop ;
this . primitiveTable [ 'broadcast:' ] = function ( b ) { interp . broadcast ( b , false ) ; } ;
this . primitiveTable [ 'doBroadcastAndWait' ] = function ( b ) { interp . broadcast ( b , true ) ; } ;
this . primitiveTable [ 'wait:elapsed:from:' ] = this . primWait ;
// added by John:
2013-11-01 22:44:51 -04:00
this . primitiveTable [ 'showBubble' ] = function ( b ) { console . log ( interp . arg ( b , 1 ) ) ; } ;
this . primitiveTable [ 'timerReset' ] = function ( b ) { interp . timerBase = Date . now ( ) ; } ;
this . primitiveTable [ 'timer' ] = function ( b ) { return ( Date . now ( ) - interp . timerBase ) / 1000 ; } ;
2013-10-28 20:00:20 +00:00
new Primitives ( ) . addPrimsTo ( this . primitiveTable ) ;
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
2013-11-01 22:44:51 -04:00
Interpreter . prototype . timerBase = Date . now ( ) ;
2013-10-28 20:00:20 +00:00
Interpreter . prototype . lookupPrim = function ( op ) {
var fcn = interp . primitiveTable [ op ] ;
2013-11-01 22:44:51 -04:00
if ( fcn == null ) fcn = function ( b ) { console . log ( 'not implemented: ' + b . op ) ; } ;
2013-10-28 20:00:20 +00:00
return fcn ;
2013-11-01 22:44:51 -04:00
} ;
Interpreter . prototype . primNoop = function ( b ) { console . log ( b . op ) ; } ;
2013-10-28 20:00:20 +00:00
Interpreter . prototype . primWait = function ( b ) {
if ( interp . activeThread . firstTime ) interp . startTimer ( interp . arg ( b , 0 ) ) ;
else interp . checkTimer ( ) ;
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
Interpreter . prototype . primRepeat = function ( b ) {
if ( b . tmp == - 1 ) {
b . tmp = Math . max ( interp . arg ( b , 0 ) , 0 ) ; // Initialize repeat count on this block
}
if ( b . tmp > 0 ) {
b . tmp -= 1 ; // decrement count
interp . startSubstack ( b , true ) ;
} else {
// Done executing this repeat block for this round
b . tmp = - 1 ;
b = null ;
}
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
Interpreter . prototype . broadcast = function ( b , waitFlag ) {
var pair ;
if ( interp . activeThread . firstTime ) {
var receivers = [ ] ;
var msg = String ( interp . arg ( b , 0 ) ) . toLowerCase ( ) ;
2013-11-01 22:44:51 -04:00
var findReceivers = function ( stack , target ) {
2013-10-28 20:00:20 +00:00
if ( ( stack . op == "whenIReceive" ) && ( stack . args [ 0 ] . toLowerCase ( ) == msg ) ) {
receivers . push ( [ stack , target ] ) ;
}
}
runtime . allStacksDo ( findReceivers ) ;
for ( pair in receivers ) interp . restartThread ( receivers [ pair ] [ 0 ] , receivers [ pair ] [ 1 ] ) ;
if ( ! waitFlag ) return ;
interp . activeThread . tmpObj = receivers ;
interp . activeThread . firstTime = false ;
}
var done = true ;
for ( pair in interp . activeThread . tmpObj ) {
if ( interp . isRunning ( interp . activeThread . tmpObj [ pair ] [ 0 ] ) ) {
done = false ;
}
2013-11-01 22:44:51 -04:00
}
2013-10-28 20:00:20 +00:00
if ( done ) {
interp . activeThread . tmpObj = null ;
interp . activeThread . firstTime = true ;
} else {
interp . yield = true ;
}
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
Interpreter . prototype . isRunning = function ( b ) {
for ( t in interp . threads ) {
if ( interp . threads [ t ] . firstBlock == b ) {
return true ;
}
}
return false ;
2013-11-01 22:44:51 -04:00
} ;
2013-10-28 20:00:20 +00:00
Interpreter . prototype . startSubstack = function ( b , isLoop , secondSubstack ) {
// Start the substack of a control structure command such as if or forever.
if ( isLoop ) {
b . isLoop = true ;
this . activeThread . stack . push ( b ) ; // remember the block that started the substack
}
2013-11-01 22:44:51 -04:00
if ( ! secondSubstack ) {
2013-10-28 20:00:20 +00:00
this . activeThread . nextBlock = b . substack ;
} else {
this . activeThread . nextBlock = b . substack2 ;
}
2013-11-01 22:44:51 -04:00
} ;