Groovy web console

subscribe to the feed Subscribe
to this
site

Gasic, a Basic interpreter

Published 1 decade ago by Tim Yates with tags BASIC closures curry
Actions Execute script  ▶ Edit in console Back to console Show/hide line numbers View recent scripts
// 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()