aboutsummaryrefslogtreecommitdiff
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
}