about summary refs log tree commit diff
diff options
context:
space:
mode:
authorMel <einebeere@gmail.com>2022-05-18 19:37:47 +0200
committerMel <einebeere@gmail.com>2022-05-18 19:37:47 +0200
commite4d68d39ce7f990895686139cd5cba20d2b2ef89 (patch)
tree8862118abb2181f3bb244daaec5ced0497bc07fd
parent089fa82b8f83695cb17b8f41d0376c03303e7663 (diff)
downloadjinx-e4d68d39ce7f990895686139cd5cba20d2b2ef89.tar.zst
jinx-e4d68d39ce7f990895686139cd5cba20d2b2ef89.zip
Handle errors gracefully in VM
-rw-r--r--pkg/bot/cmds.go10
-rw-r--r--pkg/lang/vm/errors.go58
-rw-r--r--pkg/lang/vm/exec.go124
-rw-r--r--pkg/lang/vm/stack.go52
-rw-r--r--pkg/lang/vm/value/type.go23
-rw-r--r--pkg/lang/vm/vm.go36
6 files changed, 255 insertions, 48 deletions
diff --git a/pkg/bot/cmds.go b/pkg/bot/cmds.go
index 1dfb2cb..85d780e 100644
--- a/pkg/bot/cmds.go
+++ b/pkg/bot/cmds.go
@@ -31,8 +31,14 @@ func vmCmd(b *Bot, content string, msg events.Message) error {
 
 	vm := vm.New(&bc)
 
-	vm.Run()
-	res := vm.GetResult()
+	if err := vm.Run(); err != nil {
+		return err
+	}
+
+	res, err := vm.GetResult()
+	if err != nil {
+		return err
+	}
 
 	b.logger.Debug().Msg("executed code")
 
diff --git a/pkg/lang/vm/errors.go b/pkg/lang/vm/errors.go
new file mode 100644
index 0000000..d19604a
--- /dev/null
+++ b/pkg/lang/vm/errors.go
@@ -0,0 +1,58 @@
+package vm
+
+import (
+	"errors"
+	"fmt"
+	"jinx/pkg/lang/vm/code"
+	"jinx/pkg/lang/vm/text"
+	"jinx/pkg/lang/vm/value"
+)
+
+// Fatal errors
+
+var (
+	ErrCallStackOverflow   = errors.New("call stack overflow (max depth: 1000)")
+	ErrLocalStackOverflow  = errors.New("local stack overflow (max depth: 1000)")
+	ErrNoPreviousCallFrame = errors.New("no previous call frame")
+	ErrCantPopRootFrame    = errors.New("cannot pop root frame")
+
+	ErrCallFrameEmpty = errors.New("current call frame is empty")
+)
+
+type ErrLocalIndexOutOfBounds struct {
+	Index int
+	Len   int
+}
+
+func (e ErrLocalIndexOutOfBounds) Error() string {
+	return fmt.Sprintf("local index out of bounds: %d (len: %d)", e.Index, e.Len)
+}
+
+type ErrInvalidOp struct {
+	Op uint8
+}
+
+func (e ErrInvalidOp) Error() string {
+	return fmt.Sprintf("invalid opcode: %d", e.Op)
+}
+
+// Non-fatal errors, which will later be implemented as catchable exceptions
+
+type ErrInvalidOperandTypes struct {
+	Op code.Op
+	X  value.Type
+	Y  value.Type
+}
+
+func (e ErrInvalidOperandTypes) Error() string {
+	return fmt.Sprintf("invalid operand types for op %s: %v, %v", text.OpToString(e.Op), e.X, e.Y)
+}
+
+type ErrArrayIndexOutOfBounds struct {
+	Index int
+	Len   int
+}
+
+func (e ErrArrayIndexOutOfBounds) Error() string {
+	return fmt.Sprintf("array index out of bounds: %d (len: %d)", e.Index, e.Len)
+}
diff --git a/pkg/lang/vm/exec.go b/pkg/lang/vm/exec.go
index f50d60b..c145acf 100644
--- a/pkg/lang/vm/exec.go
+++ b/pkg/lang/vm/exec.go
@@ -1,6 +1,9 @@
 package vm
 
-import "jinx/pkg/lang/vm/value"
+import (
+	"jinx/pkg/lang/vm/code"
+	"jinx/pkg/lang/vm/value"
+)
 
 func (vm *VM) execPushInt(x int64) {
 	vm.stack.Top().Push(value.NewInt(x))
@@ -26,20 +29,44 @@ func (vm *VM) execPushArray() {
 	vm.stack.Top().Push(value.NewArray([]value.Value{}))
 }
 
-func (vm *VM) execGetLocal(offset int) {
+func (vm *VM) execGetLocal(offset int) error {
 	top := vm.stack.Top()
-	top.Push(top.At(int(offset)))
+
+	local, err := top.At(int(offset))
+	if err != nil {
+		return err
+	}
+
+	top.Push(local)
+	return nil
 }
 
-func (vm *VM) execGetArg() {
-	vm.stack.Top().Push(vm.stack.Prev().Pop())
+func (vm *VM) execGetArg() error {
+	prev, err := vm.stack.Prev()
+	if err != nil {
+		return err
+	}
+
+	arg, err := prev.Pop()
+	if err != nil {
+		return err
+	}
+
+	vm.stack.Top().Push(arg)
+	return nil
 }
 
-func (vm *VM) execAdd() {
+func (vm *VM) execAdd() error {
 	top := vm.stack.Top()
 
-	x := top.Pop()
-	y := top.Pop()
+	x, err := top.Pop()
+	if err != nil {
+		return err
+	}
+	y, err := top.Pop()
+	if err != nil {
+		return err
+	}
 
 	var res value.Value
 
@@ -54,7 +81,11 @@ func (vm *VM) execAdd() {
 			yv := float64(y.Data().(value.FloatData))
 			res = value.NewFloat(float64(xv) + yv)
 		default:
-			panic("invalid operand types")
+			return ErrInvalidOperandTypes{
+				Op: code.OpAdd,
+				X:  x.Type(),
+				Y:  y.Type(),
+			}
 		}
 	case value.FloatType:
 		xv := float64(x.Data().(value.FloatData))
@@ -66,27 +97,46 @@ func (vm *VM) execAdd() {
 			yv := float64(y.Data().(value.FloatData))
 			res = value.NewFloat(xv + yv)
 		default:
-			panic("invalid operand types")
+			return ErrInvalidOperandTypes{
+				Op: code.OpAdd,
+				X:  x.Type(),
+				Y:  y.Type(),
+			}
 		}
 	case value.StringType:
 		switch y.Type().Kind {
 		case value.StringType:
 			res = value.NewString(string(x.Data().(value.StringData)) + string(y.Data().(value.StringData)))
 		default:
-			panic("invalid operand types")
+			return ErrInvalidOperandTypes{
+				Op: code.OpAdd,
+				X:  x.Type(),
+				Y:  y.Type(),
+			}
 		}
 	default:
-		panic("invalid operand types")
+		return ErrInvalidOperandTypes{
+			Op: code.OpAdd,
+			X:  x.Type(),
+			Y:  y.Type(),
+		}
 	}
 
 	top.Push(res)
+	return nil
 }
 
-func (vm *VM) execSub() {
+func (vm *VM) execSub() error {
 	top := vm.stack.Top()
 
-	x := top.Pop()
-	y := top.Pop()
+	x, err := top.Pop()
+	if err != nil {
+		return err
+	}
+	y, err := top.Pop()
+	if err != nil {
+		return err
+	}
 
 	var res value.Value
 
@@ -101,7 +151,11 @@ func (vm *VM) execSub() {
 			yv := float64(y.Data().(value.FloatData))
 			res = value.NewFloat(float64(xv) - yv)
 		default:
-			panic("invalid operand types")
+			return ErrInvalidOperandTypes{
+				Op: code.OpSub,
+				X:  x.Type(),
+				Y:  y.Type(),
+			}
 		}
 	case value.FloatType:
 		xv := float64(x.Data().(value.FloatData))
@@ -113,31 +167,55 @@ func (vm *VM) execSub() {
 			yv := float64(y.Data().(value.FloatData))
 			res = value.NewFloat(xv - yv)
 		default:
-			panic("invalid operand types")
+			return ErrInvalidOperandTypes{
+				Op: code.OpSub,
+				X:  x.Type(),
+				Y:  y.Type(),
+			}
 		}
 	default:
-		panic("invalid operand types")
+		return ErrInvalidOperandTypes{
+			Op: code.OpSub,
+			X:  x.Type(),
+			Y:  y.Type(),
+		}
 	}
 
 	top.Push(res)
+	return nil
 }
 
-func (vm *VM) execIndex() {
+func (vm *VM) execIndex() error {
 	top := vm.stack.Top()
 
-	v := top.Pop()
-	i := top.Pop()
+	v, err := top.Pop()
+	if err != nil {
+		return err
+	}
+	i, err := top.Pop()
+	if err != nil {
+		return err
+	}
 
 	switch v.Type().Kind {
 	case value.ArrayType:
 		arr := v.Data().([]value.Value)
 		idx := i.Data().(int64)
 		if idx < 0 || idx >= int64(len(arr)) {
-			panic("index out of bounds")
+			return ErrArrayIndexOutOfBounds{
+				Index: int(idx),
+				Len:   len(arr),
+			}
 		}
 
 		top.Push(arr[idx])
 	default:
-		panic("invalid operand types")
+		return ErrInvalidOperandTypes{
+			Op: code.OpIndex,
+			X:  v.Type(),
+			Y:  i.Type(),
+		}
 	}
+
+	return nil
 }
diff --git a/pkg/lang/vm/stack.go b/pkg/lang/vm/stack.go
index 1d028e5..9cf7db8 100644
--- a/pkg/lang/vm/stack.go
+++ b/pkg/lang/vm/stack.go
@@ -1,6 +1,8 @@
 package vm
 
-import "jinx/pkg/lang/vm/value"
+import (
+	"jinx/pkg/lang/vm/value"
+)
 
 type CallStack []*LocalStack
 
@@ -8,34 +10,64 @@ func NewCallStack() CallStack {
 	return []*LocalStack{{}}
 }
 
-func (cs *CallStack) Push() {
+func (cs *CallStack) Push() error {
+	if len(*cs) > 1000 {
+		return ErrCallStackOverflow
+	}
+
 	*cs = append(*cs, &LocalStack{})
+	return nil
 }
 
-func (cs *CallStack) Pop() {
+func (cs *CallStack) Pop() error {
+	if len(*cs) <= 1 {
+		return ErrCantPopRootFrame
+	}
+
 	*cs = (*cs)[:len(*cs)-1]
+	return nil
 }
 
 func (cs *CallStack) Top() *LocalStack {
 	return (*cs)[len(*cs)-1]
 }
 
-func (cs *CallStack) Prev() *LocalStack {
-	return (*cs)[len(*cs)-2]
+func (cs *CallStack) Prev() (*LocalStack, error) {
+	if len(*cs) <= 1 {
+		return nil, ErrNoPreviousCallFrame
+	}
+
+	return (*cs)[len(*cs)-2], nil
 }
 
 type LocalStack []value.Value
 
-func (ls *LocalStack) Push(v value.Value) {
+func (ls *LocalStack) Push(v value.Value) error {
+	if len(*ls) > 1000 {
+		return ErrLocalStackOverflow
+	}
+
 	*ls = append(*ls, v)
+	return nil
 }
 
-func (ls *LocalStack) Pop() value.Value {
+func (ls *LocalStack) Pop() (value.Value, error) {
+	if len(*ls) == 0 {
+		return value.Value{}, ErrCallFrameEmpty
+	}
+
 	v := (*ls)[len(*ls)-1]
 	*ls = (*ls)[:len(*ls)-1]
-	return v
+	return v, nil
 }
 
-func (ls *LocalStack) At(at int) value.Value {
-	return (*ls)[at]
+func (ls *LocalStack) At(at int) (value.Value, error) {
+	if at >= len(*ls) {
+		return value.Value{}, ErrLocalIndexOutOfBounds{
+			Index: at,
+			Len:   len(*ls),
+		}
+	}
+
+	return (*ls)[at], nil
 }
diff --git a/pkg/lang/vm/value/type.go b/pkg/lang/vm/value/type.go
index 1aec251..cd518d9 100644
--- a/pkg/lang/vm/value/type.go
+++ b/pkg/lang/vm/value/type.go
@@ -17,3 +17,26 @@ const (
 type Type struct {
 	Kind TypeKind
 }
+
+func (t Type) String() string {
+	switch t.Kind {
+	case IntType:
+		return "int"
+	case FloatType:
+		return "float"
+	case StringType:
+		return "string"
+	case BoolType:
+		return "bool"
+	case ArrayType:
+		return "array"
+	case NullType:
+		return "null"
+	case FunctionType:
+		return "function"
+	case ObjectType:
+		return "object"
+	}
+
+	panic("invalid type kind")
+}
diff --git a/pkg/lang/vm/vm.go b/pkg/lang/vm/vm.go
index 6746e64..c538841 100644
--- a/pkg/lang/vm/vm.go
+++ b/pkg/lang/vm/vm.go
@@ -19,19 +19,28 @@ func New(code *code.Code) *VM {
 	}
 }
 
-func (vm *VM) GetResult() string {
-	return fmt.Sprintf("%v", vm.stack.Top().Pop().Data())
+func (vm *VM) GetResult() (string, error) {
+	res, err := vm.stack.Top().Pop()
+	if err != nil {
+		return "", err
+	}
+
+	return fmt.Sprintf("%v", res.Data()), nil
 }
 
-func (vm *VM) Run() {
+func (vm *VM) Run() error {
 	for vm.pc < vm.code.Len() {
 		op, advance := vm.code.GetOp(vm.pc)
 		vm.pc += advance
 
-		if vm.step(op) == stepDecisionHalt {
-			return
+		if decision, err := vm.step(op); err != nil {
+			return err
+		} else if decision == stepDecisionHalt {
+			return nil
 		}
 	}
+
+	return nil
 }
 
 type stepDecision int
@@ -41,12 +50,13 @@ const (
 	stepDecisionHalt
 )
 
-func (vm *VM) step(op code.Op) stepDecision {
+func (vm *VM) step(op code.Op) (stepDecision, error) {
+	var err error
 	switch op {
 	case code.OpNop:
 		// do nothing
 	case code.OpHalt:
-		return stepDecisionHalt
+		return stepDecisionHalt, nil
 
 	case code.OpPushInt:
 		x, advance := vm.code.GetInt(vm.pc)
@@ -82,7 +92,7 @@ func (vm *VM) step(op code.Op) stepDecision {
 		offset, advance := vm.code.GetInt(vm.pc)
 		vm.pc += advance
 
-		vm.execGetLocal(int(offset))
+		err = vm.execGetLocal(int(offset))
 	case code.OpGetMember:
 		panic("not implemented")
 	case code.OpGetArg:
@@ -91,11 +101,11 @@ func (vm *VM) step(op code.Op) stepDecision {
 		panic("not implemented")
 
 	case code.OpAdd:
-		vm.execAdd()
+		err = vm.execAdd()
 	case code.OpSub:
-		vm.execSub()
+		err = vm.execSub()
 	case code.OpIndex:
-		vm.execIndex()
+		err = vm.execIndex()
 	case code.OpCall:
 		panic("not implemented")
 
@@ -109,8 +119,8 @@ func (vm *VM) step(op code.Op) stepDecision {
 		panic("not implemented")
 
 	default:
-		panic(fmt.Errorf("unimplemented op: %v", op))
+		err = ErrInvalidOp{Op: uint8(op)}
 	}
 
-	return stepDecisionContinue
+	return stepDecisionContinue, err
 }