Permalink
Tree: 4a58ec1cb4
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
1564 lines (1298 sloc) 32.3 KB
// Package eval contains our evaluator
//
// This is pretty simple:
//
// * The program is an array of tokens.
//
// * We have one statement per line.
//
// * We handle the different types of statements in their own functions.
//
package eval
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
"github.com/skx/gobasic/object"
"github.com/skx/gobasic/token"
"github.com/skx/gobasic/tokenizer"
)
// Interpreter holds our state.
type Interpreter struct {
// The program we execute is nothing more than an array of tokens.
program []token.Token
// Should we finish execution?
// This is set by the `END` statement.
finished bool
// We execute from the given offset.
//
// Sequential execution just means bumping this up by one each
// time we execute an instruction, or pick off the arguments to
// one.
//
// But set it to 17, or some other random value, and you've got
// a GOTO implemented!
offset int
// We record the line-number we're currently executing here.
// NOTE: This is a string because we take it from the lineno
// token, with no modification.
lineno string
// A stack for handling GOSUB/RETURN calls
gstack *Stack
// vars holds the variables set in the program, via LET.
vars *Variables
// loops holds references to open FOR-loops
loops *Loops
// STDIN is an input-reader used for the INPUT statement
STDIN *bufio.Reader
// Hack: Was the previous statement a GOTO/GOSUB?
jump bool
// lines is a lookup table - the key is the line-number of
// the source program, and the value is the offset in our
// program-array that this is located at.
lines map[string]int
// functions holds builtin-functions
functions *Builtins
// trace is true if the user is tracing execution
trace bool
}
// New is our constructor.
//
// Given a lexer we store all the tokens it produced in our array, and
// initialise some other state.
func New(stream *tokenizer.Tokenizer) *Interpreter {
t := &Interpreter{offset: 0}
// setup a stack for holding line-numbers for GOSUB/RETURN
t.gstack = NewStack()
// setup storage for variable-contents
t.vars = NewVars()
// setup storage for for-loops
t.loops = NewLoops()
// Built-in functions are stored here.
t.functions = NewBuiltins()
// allow reading from STDIN
t.STDIN = bufio.NewReader(os.Stdin)
//
// Setup a map to hold our jump-targets
//
t.lines = make(map[string]int)
//
// Save the tokens that our program consists of, one by one,
// until we hit the end.
//
// We also record the offset at which each line starts, which
// means that the GOTO & GOSUB statements don't need to scan
// the program from start to finish to find the destination
// to jump to.
//
offset := 0
for {
tok := stream.NextToken()
if tok.Type == token.EOF {
break
}
// Did we find a line-number?
if tok.Type == token.LINENO {
// Save the offset in the map
line := tok.Literal
// Already an offset? That means we
// have duplicate line-numbers
if t.lines[line] != 0 {
fmt.Printf("WARN: Line %s is duplicated - GOTO/GOSUB behaviour is undefined\n", line)
}
t.lines[line] = offset
}
// Regardless append the token to our array
t.program = append(t.program, tok)
offset++
}
//
// Add in our builtins.
//
// These are implemented in golang in the file builtins.go
//
// We have to do this after we've loaded our program, because
// the registration involves rewriting our program.
//
t.RegisterBuiltin("ABS", 1, ABS)
t.RegisterBuiltin("ACS", 1, ACS)
t.RegisterBuiltin("ASN", 1, ASN)
t.RegisterBuiltin("ATN", 1, ATN)
t.RegisterBuiltin("BIN", 1, BIN)
t.RegisterBuiltin("COS", 1, COS)
t.RegisterBuiltin("EXP", 1, EXP)
t.RegisterBuiltin("INT", 1, INT)
t.RegisterBuiltin("LN", 1, LN)
t.RegisterBuiltin("PI", 0, PI)
t.RegisterBuiltin("RND", 1, RND)
t.RegisterBuiltin("SGN", 1, SGN)
t.RegisterBuiltin("SIN", 1, SIN)
t.RegisterBuiltin("SQR", 1, SQR)
t.RegisterBuiltin("TAN", 1, TAN)
t.RegisterBuiltin("VAL", 1, VAL)
// Primitives that operate upon strings
t.RegisterBuiltin("CHR$", 1, CHR)
t.RegisterBuiltin("CODE", 1, CODE)
t.RegisterBuiltin("LEFT$", 2, LEFT)
t.RegisterBuiltin("LEN", 1, LEN)
t.RegisterBuiltin("MID$", 3, MID)
t.RegisterBuiltin("RIGHT$", 2, RIGHT)
t.RegisterBuiltin("TL$", 1, TL)
t.RegisterBuiltin("STR$", 1, STR)
// Output
t.RegisterBuiltin("PRINT", -1, PRINT)
t.RegisterBuiltin("DUMP", 1, DUMP)
return t
}
// SetTrace allows the user to enable/disable tracing.
func (e *Interpreter) SetTrace(val bool) {
e.trace = val
}
////
//
// Helpers for stuff
//
////
// factor
func (e *Interpreter) factor() object.Object {
if e.offset >= len(e.program) {
return object.Error("Hit end of program processing factor()")
}
tok := e.program[e.offset]
switch tok.Type {
case token.LBRACKET:
// skip past the lbracket
e.offset++
if e.offset >= len(e.program) {
return object.Error("Hit end of program processing factor()")
}
// handle the expr
ret := e.expr(true)
if ret.Type() == object.ERROR {
return ret
}
// skip past the rbracket
tok = e.program[e.offset]
if tok.Type != token.RBRACKET {
return object.Error("Unclosed bracket around expression!")
}
e.offset++
// Return the result of the sub-expression
return (ret)
case token.INT:
i, err := strconv.ParseFloat(tok.Literal, 64)
if err == nil {
e.offset++
return &object.NumberObject{Value: i}
}
return object.Error("Failed to convert %s -> float64 %s", tok.Literal, err.Error())
case token.STRING:
e.offset++
return &object.StringObject{Value: tok.Literal}
case token.BUILTIN:
//
// Call the built-in and return the value.
//
val := e.callBuiltin(tok.Literal)
return val
case token.IDENT:
//
// Get the contents of the variable.
//
val := e.GetVariable(tok.Literal)
e.offset++
return val
}
return object.Error("factor() - unhandled token: %v\n", tok)
}
// terminal - handles parsing of the form
// ARG1 OP ARG2
//
// See also expr() which is similar.
func (e *Interpreter) term() object.Object {
// First argument
f1 := e.factor()
if e.offset >= len(e.program) {
return object.Error("Hit end of program processing term()")
}
// Get the operator
tok := e.program[e.offset]
// Here we handle the obvious ones.
for tok.Type == token.ASTERISK ||
tok.Type == token.SLASH ||
tok.Type == token.MOD {
// skip the operator
e.offset++
if e.offset >= len(e.program) {
return object.Error("Hit end of program processing term()")
}
// get the second argument
f2 := e.factor()
if e.offset >= len(e.program) {
return object.Error("Hit end of program processing term()")
}
//
// We allow operations of the form:
//
// NUMBER OP NUMBER
//
// We can error on strings.
//
if f1.Type() != object.NUMBER ||
f2.Type() != object.NUMBER {
return object.Error("term() only handles integers")
}
//
// Get the values.
//
v1 := f1.(*object.NumberObject).Value
v2 := f2.(*object.NumberObject).Value
//
// Handle the operator.
//
if tok.Type == token.ASTERISK {
f1 = &object.NumberObject{Value: v1 * v2}
}
if tok.Type == token.SLASH {
if v2 == 0 {
return object.Error("Division by zero!")
}
f1 = &object.NumberObject{Value: v1 / v2}
}
if tok.Type == token.MOD {
d1 := int(v1)
d2 := int(v2)
if d2 == 0 {
return object.Error("MOD 0 is an error!")
}
f1 = &object.NumberObject{Value: float64(d1 % d2)}
}
if e.offset >= len(e.program) {
return object.Error("Hit end of program processing term()")
}
// repeat?
tok = e.program[e.offset]
}
return f1
}
// expression - handles parsing of the form
// ARG1 OP ARG2
// See also term() which is similar.
func (e *Interpreter) expr(allowBinOp bool) object.Object {
// First argument.
t1 := e.term()
// Did this error?
if t1.Type() == object.ERROR {
return t1
}
if e.offset >= len(e.program) {
return object.Error("Hit end of program processing expr()")
}
// Get the operator
tok := e.program[e.offset]
// Here we handle the obvious ones.
for tok.Type == token.PLUS ||
tok.Type == token.MINUS ||
tok.Type == token.AND ||
tok.Type == token.OR {
//
// Sometimes we disable binary AND + binary OR.
//
// This is mostly due to our naive parser, because
// it gets confused handling "IF BLAH AND BLAH .."
//
if allowBinOp == false {
if tok.Type == token.AND ||
tok.Type == token.OR {
return t1
}
}
// skip the operator
e.offset++
// Get the second argument.
t2 := e.term()
// Did this error?
if t2.Type() == object.ERROR {
return t2
}
//
// We allow operations of the form:
//
// NUMBER OP NUMBER
//
// STRING OP STRING
//
// We support ZERO operations where the operand types
// do not match. If we hit this it's a bug.
//
if t1.Type() != t2.Type() {
return object.Error("expr() - type mismatch between '%v' + '%v'", t1, t2)
}
//
// Are the operands strings?
//
if t1.Type() == object.STRING {
//
// Get their values.
//
s1 := t1.(*object.StringObject).Value
s2 := t2.(*object.StringObject).Value
//
// We only support "+" for concatenation
//
if tok.Type == token.PLUS {
t1 = &object.StringObject{Value: s1 + s2}
} else {
return object.Error("expr() operation '%s' not supported for strings", tok.Literal)
}
} else {
//
// Here we have two operands that are numbers.
//
// Get their values for neatness.
//
n1 := t1.(*object.NumberObject).Value
n2 := t2.(*object.NumberObject).Value
if tok.Type == token.PLUS {
t1 = &object.NumberObject{Value: n1 + n2}
} else if tok.Type == token.MINUS {
t1 = &object.NumberObject{Value: n1 - n2}
} else if tok.Type == token.AND {
t1 = &object.NumberObject{Value: float64(int(n1) & int(n2))}
} else if tok.Type == token.OR {
t1 = &object.NumberObject{Value: float64(int(n1) | int(n2))}
} else {
return object.Error("Token not handled for two numbers: %s\n", tok.Literal)
}
}
// repeat?
tok = e.program[e.offset]
}
return t1
}
// compare runs a comparison function (!)
//
// It is only used by the `IF` statement.
func (e *Interpreter) compare(allowBinOp bool) object.Object {
// Get the first statement
t1 := e.expr(allowBinOp)
if t1.Type() == object.ERROR {
return t1
}
// Get the comparison function
op := e.program[e.offset]
// If the next token is an IF then we're going
// to regard the test as a pass if the first
// value was not 0 (number) and not "" (string)
if op.Type == token.THEN {
switch t1.Type() {
case object.STRING:
if "" != t1.(*object.StringObject).Value {
return &object.NumberObject{Value: 1}
}
case object.NUMBER:
if 0 != t1.(*object.NumberObject).Value {
return &object.NumberObject{Value: 1}
}
}
return &object.NumberObject{Value: 0}
}
//
// OK bump past the comparision function.
//
e.offset++
if e.offset >= len(e.program) {
return object.Error("Hit end of program processing compare()")
}
// Get the second expression
t2 := e.expr(allowBinOp)
if t2.Type() == object.ERROR {
return t2
}
//
// String-tests here
//
if t1.Type() == object.STRING && t2.Type() == object.STRING {
v1 := t1.(*object.StringObject).Value
v2 := t2.(*object.StringObject).Value
switch op.Type {
case token.ASSIGN:
if v1 == v2 {
//true
return &object.NumberObject{Value: 1}
}
case token.NOT_EQUALS:
if v1 != v2 {
//true
return &object.NumberObject{Value: 1}
}
case token.GT:
if v1 > v2 {
//true
return &object.NumberObject{Value: 1}
}
case token.GT_EQUALS:
if v1 >= v2 {
//true
return &object.NumberObject{Value: 1}
}
case token.LT:
if v1 < v2 {
//true
return &object.NumberObject{Value: 1}
}
case token.LT_EQUALS:
if v1 <= v2 {
//true
return &object.NumberObject{Value: 1}
}
}
// false
return &object.NumberObject{Value: 0}
}
//
// String-tests here
//
if t1.Type() == object.NUMBER && t2.Type() == object.NUMBER {
v1 := t1.(*object.NumberObject).Value
v2 := t2.(*object.NumberObject).Value
switch op.Type {
case token.ASSIGN:
if v1 == v2 {
//true
return &object.NumberObject{Value: 1}
}
case token.GT:
if v1 > v2 {
//true
return &object.NumberObject{Value: 1}
}
case token.GT_EQUALS:
if v1 >= v2 {
//true
return &object.NumberObject{Value: 1}
}
case token.LT:
if v1 < v2 {
//true
return &object.NumberObject{Value: 1}
}
case token.LT_EQUALS:
if v1 <= v2 {
//true
return &object.NumberObject{Value: 1}
}
case token.NOT_EQUALS:
if v1 != v2 {
//true
return &object.NumberObject{Value: 1}
}
}
// false
return &object.NumberObject{Value: 0}
}
return object.Error("Unhandled comparison: %v[%s] %v %v[%s]\n", t1, t1.Type(), op, t2, t2.Type())
}
// Call the built-in with the given name if we can.
func (e *Interpreter) callBuiltin(name string) object.Object {
if e.trace {
fmt.Printf("callBultin(%s)\n", name)
}
//
// Fetch the function, so we know how many arguments
// it should expect.
//
n, fun := e.functions.Get(name)
//
// skip past the function-call itself
//
e.offset++
if e.offset >= len(e.program) {
return object.Error("Hit end of program processing builtin %s", name)
}
//
// Each built-in takes a specific number of arguments.
//
// We pass only `string` or `number` to it.
//
var args []object.Object
//
// Build up the args, converting and evaluating as we go.
//
for n == -1 || len(args) < n {
if e.offset >= len(e.program) {
return object.Error("Hit end of program processing builtin %s", name)
}
//
// Get the next token, if it is a comma then eat it.
//
tok := e.program[e.offset]
if tok.Type == token.COMMA {
//
// Hack
//
if name == "PRINT" {
args = append(args, &object.StringObject{Value: " "})
}
e.offset++
continue
}
//
// If we've hit a colon, or a newline we're done.
//
//
// If we hit newline/eof then we're done.
//
// (And we've got an error, because we didn't receive as
// many arguments as we expected.)
//
if tok.Type == token.NEWLINE {
if n > 0 {
return (object.Error("Hit newline while searching for argument %d to %s", len(args)+1, name))
} else {
break
}
}
if tok.Type == token.COLON {
if n > 0 {
return (object.Error("Hit ':' while searching for argument %d to %s", len(args)+1, name))
} else {
break
}
}
if tok.Type == token.EOF {
if n > 0 {
return (object.Error("Hit EOF while searching for argument %d to %s", len(args)+1, name))
} else {
break
}
}
//
// Evaluate the next expression.
//
obj := e.expr(true)
//
// If we found an error then return it.
//
if obj.Type() == object.ERROR {
return obj
}
//
// Append the argument to our list.
//
args = append(args, obj)
//
// Show our current progress.
//
if e.trace {
fmt.Printf("\tArgument %d -> %s\n", len(args), obj.String())
}
}
//
// Actually call the function, now we have the correct number
// of arguments to do so.
//
out := fun(*e, args)
if e.trace {
fmt.Printf("\tReturn value %s\n", out.String())
}
return out
}
////
//
// Statement-handlers
//
////
// runForLoop handles a FOR loop
func (e *Interpreter) runForLoop() error {
// we expect "ID = NUM to NUM [STEP NUM]"
// Bump past the FOR token
e.offset++
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing FOR")
}
// We now expect a variable name.
target := e.program[e.offset]
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing FOR")
}
e.offset++
if target.Type != token.IDENT {
return fmt.Errorf("Expected IDENT after FOR, got %v", target)
}
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing FOR")
}
// Now an EQUALS
eq := e.program[e.offset]
e.offset++
if eq.Type != token.ASSIGN {
return fmt.Errorf("Expected = after 'FOR %s' , got %v", target.Literal, eq)
}
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing FOR")
}
// Now an integer/variable
startI := e.program[e.offset]
e.offset++
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing FOR")
}
var start float64
if startI.Type == token.INT {
v, err := strconv.ParseFloat(startI.Literal, 64)
if err != nil {
return fmt.Errorf("Failed to convert %s to an int %s", startI.Literal, err.Error())
}
start = v
} else if startI.Type == token.IDENT {
x := e.GetVariable(startI.Literal)
if x.Type() != object.NUMBER {
return fmt.Errorf("FOR: start-variable must be an integer!")
}
start = x.(*object.NumberObject).Value
} else {
return fmt.Errorf("Expected INT/VARIABLE after 'FOR %s=', got %v", target.Literal, startI)
}
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing FOR")
}
// Now TO
to := e.program[e.offset]
e.offset++
if to.Type != token.TO {
return fmt.Errorf("Expected TO after 'FOR %s=%s', got %v", target.Literal, startI, to)
}
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing FOR")
}
// Now an integer/variable
endI := e.program[e.offset]
e.offset++
var end int
if endI.Type == token.INT {
v, err := strconv.ParseFloat(endI.Literal, 64)
if err != nil {
return fmt.Errorf("Failed to convert %s to an int %s", endI.Literal, err.Error())
}
end = int(v)
} else if endI.Type == token.IDENT {
x := e.GetVariable(endI.Literal)
if x.Type() != object.NUMBER {
return fmt.Errorf("FOR: end-variable must be an integer!")
}
end = int(x.(*object.NumberObject).Value)
} else {
return fmt.Errorf("Expected INT/VARIABLE after 'FOR %s=%s TO', got %v", target.Literal, startI, endI)
}
// Default step is 1.
stepI := "1"
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing FOR")
}
// Is the next token a step?
if e.program[e.offset].Type == token.STEP {
e.offset++
s := e.program[e.offset]
e.offset++
if s.Type != token.INT {
return fmt.Errorf("Expected INT after 'FOR %s=%s TO %s STEP', got %v", target.Literal, startI, endI, s)
}
stepI = s.Literal
}
step, err := strconv.ParseFloat(stepI, 64)
if err != nil {
return fmt.Errorf("Failed to convert %s to an int %s", stepI, err.Error())
}
//
// Now we can record the important details of the for-loop
// in a hash.
//
// The key observersions here are that all the magic
// really involved in the FOR-loop happens at the point
// you interpret the "NEXT X" section.
//
// Handling the NEXT statement involves:
//
// Incrementing the step-variable
// Looking for termination
// If not over then "jumping back".
//
// So for a for-loop we just record the start/end conditions
// and the address of the body of the loop - ie. the next
// token - so that the next-handler can GOTO there.
//
// It is almost beautifully elegent.
//
f := ForLoop{id: target.Literal,
offset: e.offset,
start: int(start),
end: int(end),
step: int(step)}
//
// Set the variable to the starting-value
//
e.SetVariable(target.Literal, &object.NumberObject{Value: start})
//
// And record our loop - keyed on the name of the variable
// which is used as the index. This allows easy and natural
// nested-loops.
//
// Did I say this is elegent?
//
e.loops.Add(f)
return nil
}
// runGOSUB handles a control-flow change
func (e *Interpreter) runGOSUB() error {
// Skip the GOSUB-instruction itself
e.offset++
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing GOSUB")
}
// Get the target
target := e.program[e.offset]
// We expect the target to be an int
if target.Type != token.INT {
return (fmt.Errorf("ERROR: GOSUB should be followed by an integer"))
}
//
// We want to store the return address on our GOSUB-stack,
// so that the next RETURN will continue execution at the
// next instruction.
//
// Because we only support one statement per-line we can
// handle that by bumping forward. That should put us on the
// LINENO of the following-line.
//
e.offset++
e.gstack.Push(e.offset)
//
// Lookup the offset of the given line-number in our program.
//
offset := e.lines[target.Literal]
//
// If we found it then use it.
//
if offset > 0 {
e.offset = offset
return nil
}
return fmt.Errorf("Failed to GOSUB %s", target.Literal)
}
// runGOTO handles a control-flow change
func (e *Interpreter) runGOTO() error {
// Skip the GOTO-instruction
e.offset++
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing GOTO")
}
// Get the GOTO-target
target := e.program[e.offset]
// We expect the target to be an int
if target.Type != token.INT {
return fmt.Errorf("ERROR: GOTO should be followed by an integer")
}
//
// Lookup the offset of the given line-number in our program.
//
offset := e.lines[target.Literal]
//
// If we found it then use it.
//
if offset > 0 {
e.offset = offset
return nil
}
return fmt.Errorf("Failed to GOTO %s", target.Literal)
}
// runINPUT handles input of numbers from the user.
//
// NOTE:
// INPUT "Foo", a -> Reads an integer
// INPUT "Foo", a$ -> Reads a string
func (e *Interpreter) runINPUT() error {
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing INPUT")
}
// Skip the INPUT-instruction
e.offset++
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing INPUT")
}
// Get the prompt
prompt := e.program[e.offset]
e.offset++
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing INPUT")
}
// We expect a comma
comma := e.program[e.offset]
e.offset++
if comma.Type != token.COMMA {
return fmt.Errorf("ERROR: INPUT should be : INPUT \"prompt\",var")
}
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing INPUT")
}
// Now the ID
ident := e.program[e.offset]
e.offset++
if ident.Type != token.IDENT {
return fmt.Errorf("ERROR: INPUT should be : INPUT \"prompt\",var")
}
//
// Print the prompt
//
fmt.Printf(prompt.Literal)
//
// Read the input from the user.
//
input, _ := e.STDIN.ReadString('\n')
input = strings.TrimRight(input, "\n")
//
// Now we handle the type-conversion.
//
if strings.HasSuffix(ident.Literal, "$") {
// We set a string
e.SetVariable(ident.Literal, &object.StringObject{Value: input})
return nil
}
// We set an int
i, err := strconv.ParseFloat(input, 64)
if err != nil {
return err
}
//
// Set the value
//
e.SetVariable(ident.Literal, &object.NumberObject{Value: i})
return nil
}
// runIF handles conditional testing.
//
// There are a lot of choices to be made when it comes to IF, such as
// whether to support an ELSE section or not. And what to allow
// inside the matching section generally:
//
// A single statement?
// A block?
//
// Here we _only_ allow:
//
// IF $EXPR THEN $STATEMENT ELSE $STATEMENT NEWLINE
//
// $STATEMENT will only be a single expression
//
func (e *Interpreter) runIF() error {
// Bump past the IF token
e.offset++
// Get the result of the comparison-function
// against the two arguments.
res := e.compare(false)
// Error?
if res.Type() == object.ERROR {
return fmt.Errorf("%s", res.(*object.ErrorObject).Value)
}
//
// We need a boolean result, so we convert here.
//
result := false
if res.Type() == object.NUMBER {
result = (res.(*object.NumberObject).Value == 1)
}
//
// The general form of an IF statement is
// IF $COMPARE THEN .. ELSE .. NEWLINE
//
// However we also want to allow people to write:
//
// IF A=3 OR A=4 THEN ..
//
// So we'll special case things here.
//
// We now expect THEN most of the time
target := e.program[e.offset]
e.offset++
for target.Type == token.AND ||
target.Type == token.OR {
//
// See what the next comparison looks like.
//
extra := e.compare(false)
if extra.Type() == object.ERROR {
return fmt.Errorf("%s", extra.(*object.ErrorObject).Value)
}
//
// We need a boolean answer.
//
extraResult := false
if extra.Type() == object.NUMBER {
extraResult = (extra.(*object.NumberObject).Value == 1)
}
//
// Update our result appropriately.
//
if target.Type == token.AND {
result = result && extraResult
}
if target.Type == token.OR {
result = result || extraResult
}
// Repeat?
target = e.program[e.offset]
e.offset++
}
//
// Now we're in the THEN section.
//
if target.Type != token.THEN {
return fmt.Errorf("Expected THEN after IF EXPR, got %v", target)
}
//
// OK so if our comparison succeeded we can execute the single
// statement between THEN + ELSE
//
// Otherwise between ELSE + Newline
//
if result {
//
// Execute single statement
//
e.RunOnce()
//
// Help me, I'm in Hell.
//
e.offset -= 1
//
// If the user made a jump then we'll
// abort here, because if the single-statement modified our
// flow control we're screwed.
//
// (Because we'll start searching from the NEW location.)
//
//
if e.jump {
return nil
}
//
// We get the next token, it should either be ELSE + expr
// or newline.
//
// Skip until we hit the end of line.
//
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing IF")
}
tmp := e.program[e.offset]
e.offset++
for tmp.Type != token.NEWLINE {
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing IF")
}
tmp = e.program[e.offset]
e.offset++
}
} else {
//
// Here the test failed.
//
// Skip over the truthy-condition until we either
// hit ELSE, or the newline that will terminate our
// IF-statement.
//
//
for {
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing IF")
}
tmp := e.program[e.offset]
e.offset++
// If we hit the newline then we're done
if tmp.Type == token.NEWLINE {
return nil
}
// Otherwise did we hit the else?
if tmp.Type == token.ELSE {
// Execute the single statement
e.RunOnce()
// Then return.
return nil
}
}
}
return nil
}
// runLET handles variable creation/updating.
func (e *Interpreter) runLET() error {
// Bump past the LET token
e.offset++
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing LET")
}
// We now expect an ID
target := e.program[e.offset]
e.offset++
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing LET")
}
if target.Type != token.IDENT {
return fmt.Errorf("Expected IDENT after LET, got %v", target)
}
// Now "="
assign := e.program[e.offset]
if assign.Type != token.ASSIGN {
return fmt.Errorf("Expected assignment after LET x, got %v", assign)
}
e.offset++
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing LET")
}
// now we're at the expression/value/whatever
res := e.expr(true)
// Did we get an error in the expression?
if res.Type() == object.ERROR {
return fmt.Errorf("%s", res.(*object.ErrorObject).Value)
}
// Store the result
e.SetVariable(target.Literal, res)
return nil
}
// runNEXT handles the NEXT statement
func (e *Interpreter) runNEXT() error {
// Bump past the NEXT token
e.offset++
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing NEXT")
}
// Get the identifier
target := e.program[e.offset]
e.offset++
if target.Type != token.IDENT {
return fmt.Errorf("Expected IDENT after NEXT in FOR loop, got %v", target)
}
// OK we've found the tail of a loop
//
// We need to bump the value of the given variable by the offset
// and compare it against the max.
//
// If the max hasn't finished we loop around again.
//
// If it has we remove the for-loop
//
data := e.loops.Get(target.Literal)
if data.id == "" {
return fmt.Errorf("NEXT %s found - without opening FOR", target.Literal)
}
//
// Get the variable value, and increase it.
//
cur := e.GetVariable(target.Literal)
if cur.Type() != object.NUMBER {
return fmt.Errorf("NEXT variable %s is not a number!", target.Literal)
}
iVal := cur.(*object.NumberObject).Value
//
// If the start/end offsets are the same then
// we terminate immediately.
//
if data.start == data.end {
data.finished = true
// updates-in-place. bad name
e.loops.Add(data)
}
//
// Increment the number.
//
iVal += float64(data.step)
//
// Set it
//
e.SetVariable(target.Literal, &object.NumberObject{Value: iVal})
//
// Have we finnished?
//
if data.finished {
e.loops.Remove(target.Literal)
return nil
}
//
// If we've reached our limit we mark this as complete,
// but note that we dont' terminate to allow the actual
// end-number to be inclusive.
//
if iVal == float64(data.end) {
data.finished = true
// updates-in-place. bad name
e.loops.Add(data)
}
//
// Otherwise loop again
//
e.offset = data.offset
return nil
}
// REM handles a REM statement
//
// This merely swallows input until the following newline / EOF.
func (e *Interpreter) runREM() error {
for e.offset < len(e.program) {
tok := e.program[e.offset]
if tok.Type == token.NEWLINE {
return nil
}
e.offset++
}
return nil
}
// RETURN handles a control-flow operation
func (e *Interpreter) runRETURN() error {
// Stack can't be empty
if e.gstack.Empty() {
return fmt.Errorf("RETURN without GOSUB")
}
// Get the return address
ret, err := e.gstack.Pop()
if err != nil {
return fmt.Errorf("Error handling RETURN: %s", err.Error())
}
// Return execution where we left off.
e.offset = ret
return nil
}
////
//
// Our core public API
//
////
// RunOnce executes a single statement.
func (e *Interpreter) RunOnce() error {
if e.offset >= len(e.program) {
return fmt.Errorf("Hit end of program processing RunOnce()")
}
//
// Get the current token
//
tok := e.program[e.offset]
var err error
if e.trace {
fmt.Printf("RunOnce( %s )\n", tok.String())
}
e.jump = false
//
// Handle this token
//
switch tok.Type {
case token.NEWLINE:
// NOP
case token.LINENO:
e.lineno = tok.Literal
case token.END:
e.finished = true
return nil
case token.FOR:
err = e.runForLoop()
case token.GOSUB:
err = e.runGOSUB()
e.jump = true
case token.GOTO:
err = e.runGOTO()
e.jump = true
case token.INPUT:
err = e.runINPUT()
case token.IF:
err = e.runIF()
case token.LET:
err = e.runLET()
case token.NEXT:
err = e.runNEXT()
case token.REM:
err = e.runREM()
case token.RETURN:
err = e.runRETURN()
case token.BUILTIN:
obj := e.callBuiltin(tok.Literal)
if obj.Type() == object.ERROR {
return fmt.Errorf("%s", obj.(*object.ErrorObject).Value)
}
e.offset--
default:
err = fmt.Errorf("Token not handled: %v", tok)
}
//
// Ready for the next instruction
//
e.offset++
//
// Error?
//
if err != nil {
return err
}
return nil
}
// Run launches the program, and does not return until it is over.
//
// A program will terminate when the control reaches the end of the
// final-line, or when the "END" token is encountered.
func (e *Interpreter) Run() error {
//
// We walk our series of tokens.
//
for e.offset < len(e.program) && !e.finished {
err := e.RunOnce()
if err != nil {
return fmt.Errorf("Line %s : %s", e.lineno, err.Error())
return err
}
}
//
// Here we've finished with no error, but we want to
// alert on unclosed FOR-loops.
//
if !e.loops.Empty() {
return fmt.Errorf("Unclosed FOR loop")
}
return nil
}
// SetVariable sets the contents of a variable in the interpreter environment.
//
// Useful for testing/embedding.
//
func (e *Interpreter) SetVariable(id string, val object.Object) {
e.vars.Set(id, val)
}
// GetVariable returns the contents of the given variable.
//
// Useful for testing/embedding.
//
func (e *Interpreter) GetVariable(id string) object.Object {
val := e.vars.Get(id)
if val != nil {
return val
}
return object.Error("The variable '%s' doesn't exist", id)
}
// RegisterBuiltin registers a function as a built-in, so that it can
// be called from the users' BASIC program.
//
// Useful for embedding.
//
func (e *Interpreter) RegisterBuiltin(name string, nArgs int, ft BuiltinSig) {
// Register the built-in - both lower-case and upper-case
e.functions.Register(strings.ToLower(name), nArgs, ft)
e.functions.Register(strings.ToUpper(name), nArgs, ft)
// Now ensure that in the future if we hit this built-in
// we regard it as a function-call, not a variable
for i := 0; i < len(e.program); i++ {
// Is this token a reference to the function
// as an ident?
if e.program[i].Type == token.IDENT &&
e.program[i].Literal == name {
// Change the type. (Hack!)
e.program[i].Type = token.BUILTIN
}
}
}