 |
Subscribe to this site |
|
Gasic, a Basic interpreter
// A quick Groovy port of Jasic originally by Robert Nystrom
//
// http://journal.stuffwithstuff.com/2010/07/18/jasic-a-complete-interpreter-in-one-java-file/
// http://bitbucket.org/munificent/jasic/src/tip/com/stuffwithstuff/Jasic.java
//
// I mostly did this to look into using curried closures as expression blocks in the AST step
//
// One thing you gain is Groovy truth for expressions, and that you can do STR * NUM to repeat strings
enum TokenType {
WORD, NUMBER, STRING, LABEL, LINE, EQUALS, OPERATOR, LEFT_PAREN, RIGHT_PAREN, EOF
}
@Immutable class Token {
String text
TokenType type
}
class Parser {
// All of our expressions, values and statements are closures that get curried into the Basic AST
def stringValue = { val -> "$val" }
def numberValue = { val -> val as Double }
def assignStmt = { name, expr -> variables."$name" = expr() }
def printStmt = { expr -> println expr() }
def inputStmt = { name -> variables."$name" = System.console().readLine() }
def gotoStmt = { label -> currentStatement = labels."$label" ?: currentStatement }
def ifthenStmt = { expr, label -> if( labels."$label" && expr() ) gotoStmt( label ) }
def variableExpr = { name -> variables."$name" == null ? 0 : variables."$name" }
def operatorExpr = { leftExpr, op, rightExpr ->
def ret
switch( op ) {
case '=': ret = leftExpr() == rightExpr() ; break
case '+': ret = leftExpr() + rightExpr() ; break
case '-': ret = leftExpr() - rightExpr() ; break
case '*': ret = leftExpr() * ( rightExpr() as Double ) ; break
case '/': ret = ( leftExpr() as Double ) / ( rightExpr() as Double ) ; break
case '<': ret = leftExpr() < rightExpr() ; break
case '>': ret = leftExpr() > rightExpr() ; break
default :
throw new Error("Unknown operator.")
}
ret
}
TokenList tokens
List statements = []
Map labels = [:]
int token = 0
Map variables = [:]
def currentStatement = 0
def expression() {
operator()
}
def operator() {
def expression = atomic()
while( match( TokenType.OPERATOR ) || match( TokenType.EQUALS ) ) {
def operator = last(1).text[ 0 ]
def right = atomic()
expression = operatorExpr.curry( expression, operator, right )
}
expression
}
def atomic() {
if( match( TokenType.WORD ) ) {
variableExpr.curry( last(1).text )
} else if( match( TokenType.NUMBER ) ) {
numberValue.curry( last(1).text )
} else if( match( TokenType.STRING ) ) {
stringValue.curry( last(1).text )
} else if (match(TokenType.LEFT_PAREN)) {
def expression = expression()
consume( TokenType.RIGHT_PAREN )
expression
}
else throw new Error("Couldn't parse :(")
}
boolean match( TokenType type1, TokenType type2 ) {
if( get(0).type == type1 && get(1).type == type2 ) token += 2 else false
}
boolean match( TokenType type ) {
if( get(0).type == type ) token++ else false
}
boolean match( String name ) {
if( get(0).type == TokenType.WORD && get(0).text.equals( name ) ) token++ else false
}
Token consume( TokenType type ) {
if( get(0).type != type ) throw new Error( "Expected $type." )
tokens[ token++ ]
}
Token consume( String name ) {
if( !match( name ) ) throw new Error( "Expected $name." )
last(1)
}
Token last( int offset ) {
tokens[ token - offset ]
}
Token get( int offset ) {
if( token + offset >= tokens.size() ) {
new Token( '', TokenType.EOF )
}
else tokens[ token + offset ]
}
def build() {
while( true ) {
while( match( TokenType.LINE ) ) {}
if( match( TokenType.LABEL ) ) {
labels."${last(1).text}" = statements.size()
} else if( match( TokenType.WORD, TokenType.EQUALS ) ) {
statements << assignStmt.curry( last(2).text, expression() )
} else if( match( "print" ) ) {
statements << printStmt.curry( expression() )
} else if( match( "input" ) ) {
statements << inputStmt.curry( consume( TokenType.WORD ).text )
} else if( match( "goto" ) ) {
statements << gotoStmt.curry( consume(TokenType.WORD).text )
} else if( match( "if" ) ) {
def condition = expression()
consume( "then" )
String label = consume( TokenType.WORD ).text
statements << ifthenStmt.curry( condition, label )
} else {
break
}
}
}
def interpret() {
currentStatement = 0
while( currentStatement < statements.size() ) {
def thisStatement = currentStatement
currentStatement++
statements[ thisStatement ]()
}
}
}
class TokenList {
@Delegate List tokens = []
enum TokenizeState {
DEFAULT, WORD, NUMBER, STRING, COMMENT
}
def tokenize( String text ) {
def reader = new PushbackReader( new StringReader( text ) )
def charTokens = [
'\n': TokenType.LINE,
'(' : TokenType.LEFT_PAREN,
')' : TokenType.RIGHT_PAREN,
'=' : TokenType.EQUALS,
'+' : TokenType.OPERATOR,
'-' : TokenType.OPERATOR,
'*' : TokenType.OPERATOR,
'/' : TokenType.OPERATOR,
'<' : TokenType.OPERATOR,
'>' : TokenType.OPERATOR,
]
def c
def state = TokenizeState.DEFAULT
def token = ''
// Reset is called when we have finished a token.
// Can optionally push the last char back onto the stream
def reset = { pushback = false ->
token = ''
state = TokenizeState.DEFAULT
if( pushback ) { reader.unread( [ c ] as char[] ) }
}
// Read all the chars one at a time
while( ( c = reader.read() ) > 0 ) {
c = (char)c
switch( state ) {
case TokenizeState.DEFAULT :
if( charTokens."$c" ) {
tokens << new Token( "$c", charTokens."$c" )
} else if( c ==~ /[a-zA-Z]/ ) {
token += c
state = TokenizeState.WORD
} else if( c ==~ /[0-9]/ ) {
token += c
state = TokenizeState.NUMBER
} else if( c == '"' ) {
state = TokenizeState.STRING
} else if( c == '\'' ) {
state = TokenizeState.COMMENT
}
break
case TokenizeState.WORD :
if( c ==~ /[a-zA-Z0-9]/ ) {
token += c
} else if( c == ':' ) {
tokens << new Token( token, TokenType.LABEL )
reset()
} else {
tokens << new Token( token, TokenType.WORD )
reset( true )
}
break
case TokenizeState.NUMBER :
if( c ==~ /[0-9]/ ) {
token += c
} else {
tokens << new Token( token, TokenType.NUMBER )
reset( true )
}
break
case TokenizeState.STRING :
if( c == '"' ) {
tokens << new Token( token, TokenType.STRING )
reset()
} else {
token += c
}
break
case TokenizeState.COMMENT :
if( c == '\n' ) {
reset()
}
break
}
}
}
}
def prg = """
' initialize the loop counter
count = 10
' stop looping if we're done
top:
if count = 0 then end
print "Hello, world!"
' decrement and restart the loop
count = count - 1
goto top
end:
"""
def tokenList = new TokenList()
tokenList.tokenize( prg )
def parser = new Parser( tokens:tokenList )
parser.build()
parser.interpret()