Skip to content

Commit

Permalink
fix indexable object types and add index ranges
Browse files Browse the repository at this point in the history
  • Loading branch information
MarkusFreitag committed Feb 13, 2022
1 parent 320f0e7 commit 170e6a2
Show file tree
Hide file tree
Showing 13 changed files with 729 additions and 14 deletions.
2 changes: 1 addition & 1 deletion ast/assign.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

type Assign struct {
Token token.Token
Name *Identifier
Name Expression
Value Expression
}

Expand Down
1 change: 1 addition & 0 deletions ast/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type Index struct {
Token token.Token
Left Expression
Index Expression
Colon string
}

func (ie *Index) TokenLiteral() string { return ie.Token.Literal }
Expand Down
76 changes: 75 additions & 1 deletion evaluator/assign.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package evaluator

import (
"fmt"
"math"

"github.com/flipez/rocket-lang/ast"
"github.com/flipez/rocket-lang/object"
)
Expand All @@ -11,6 +14,77 @@ func evalAssign(a *ast.Assign, env *object.Environment) (val object.Object) {
return evaluated
}

env.Set(a.Name.String(), evaluated)
switch v := a.Name.(type) {
case *ast.Identifier:
env.Set(v.String(), evaluated)
case *ast.Index:
obj, _ := env.Get(v.Left.String())
switch o := obj.(type) {
case *object.Array:
idx, err := handleIntegerIndex(v, env)
if err != nil {
return object.NewError(err)
}

if l := int64(len(o.Elements)); int64(math.Abs(float64(idx))) >= l {
return object.NewErrorFormat(
"index out of range, got %d but array has only %d elements", idx, l,
)
}

if idx < 0 {
idx = int64(len(o.Elements)) + idx
}

o.Elements[idx] = evaluated
case *object.Hash:
obj := Eval(v.Index, env)
h, ok := obj.(object.Hashable)
if !ok {
return object.NewErrorFormat("expected index to be hashable")
}

key := h.HashKey()
o.Pairs[key] = object.HashPair{Key: obj, Value: evaluated}
case *object.String:
idx, err := handleIntegerIndex(v, env)
if err != nil {
return object.NewError(err)
}

if l := int64(len(o.Value)); int64(math.Abs(float64(idx))) >= l {
return object.NewErrorFormat(
"index out of range, got %d but string is only %d long", idx, l,
)
}

if idx < 0 {
idx = int64(len(o.Value)) + idx
}

strEval, ok := evaluated.(*object.String)
if !ok {
return object.NewErrorFormat("expected STRING object, got %s", evaluated.Type())
}
if l := len(strEval.Value); l != 1 {
return object.NewErrorFormat(
"expected STRING object to have a length of 1, got %d", l,
)
}

o.Value = o.Value[:idx] + strEval.Value + o.Value[idx+1:]
default:
return object.NewErrorFormat("expected object to be indexable")
}
}
return evaluated
}

func handleIntegerIndex(ai *ast.Index, env *object.Environment) (int64, error) {
obj := Eval(ai.Index, env)
num, ok := obj.(*object.Integer)
if !ok {
return 0, fmt.Errorf("expected index to be an INTEGER, got %s", obj.Type())
}
return num.Value, nil
}
280 changes: 280 additions & 0 deletions evaluator/assign_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
package evaluator

import (
"testing"

"github.com/flipez/rocket-lang/ast"
"github.com/flipez/rocket-lang/object"
)

func newAstAssign(name, value ast.Expression) *ast.Assign {
return &ast.Assign{
Name: name,
Value: value,
}
}

func prefilledEnv(objs map[string]object.Object) *object.Environment {
env := object.NewEnvironment()
for name, obj := range objs {
env.Set(name, obj)
}
return env
}

func TestEvalAssign(t *testing.T) {
testcases := []struct {
env *object.Environment
a *ast.Assign
expected object.Object
}{
// valid string assignment
{
a: newAstAssign(
&ast.Identifier{Value: "s"},
&ast.String{Value: "abc"},
),
expected: object.NewString("abc"),
},
// assignment with an invalid expression
{
a: newAstAssign(
&ast.Identifier{Value: "s"},
&ast.Infix{
Left: &ast.String{Value: "abc"},
Operator: "+",
Right: &ast.Integer{Value: 123},
},
),
expected: object.NewErrorFormat("type mismatch: STRING + INTEGER"),
},
// valid array assignment with positive index
{
env: prefilledEnv(map[string]object.Object{
"a": object.NewArrayWithObjects(
object.NewString("a"),
object.NewString("b"),
object.NewString("c"),
),
}),
a: newAstAssign(
&ast.Index{
Left: &ast.Identifier{Value: "a"},
Index: &ast.Integer{Value: 0},
},
&ast.String{Value: "A"},
),
expected: object.NewString("A"),
},
// valid array assignment with negative index
{
env: prefilledEnv(map[string]object.Object{
"a": object.NewArrayWithObjects(
object.NewString("a"),
object.NewString("b"),
object.NewString("c"),
),
}),
a: newAstAssign(
&ast.Index{
Left: &ast.Identifier{Value: "a"},
Index: &ast.Integer{Value: -1},
},
&ast.String{Value: "C"},
),
expected: object.NewString("C"),
},
// array assignment with non integer index
{
env: prefilledEnv(map[string]object.Object{
"a": object.NewArrayWithObjects(
object.NewString("a"),
object.NewString("b"),
object.NewString("c"),
),
}),
a: newAstAssign(
&ast.Index{
Left: &ast.Identifier{Value: "a"},
Index: &ast.String{Value: "1"},
},
&ast.String{Value: "C"},
),
expected: object.NewErrorFormat("expected index to be an INTEGER, got STRING"),
},
// array assignment with integer index bigger than array size
{
env: prefilledEnv(map[string]object.Object{
"a": object.NewArrayWithObjects(
object.NewString("a"),
object.NewString("b"),
object.NewString("c"),
),
}),
a: newAstAssign(
&ast.Index{
Left: &ast.Identifier{Value: "a"},
Index: &ast.Integer{Value: 3},
},
&ast.String{Value: "C"},
),
expected: object.NewErrorFormat("index out of range, got 3 but array has only 3 elements"),
},
// valid hash assignment
{
env: prefilledEnv(map[string]object.Object{
"h": object.NewHash(map[object.HashKey]object.HashPair{
object.NewString("a").HashKey(): object.HashPair{
Key: object.NewString("a"),
Value: object.NewInteger(1),
},
}),
}),
a: newAstAssign(
&ast.Index{
Left: &ast.Identifier{Value: "h"},
Index: &ast.String{Value: "a"},
},
&ast.Integer{Value: 2},
),
expected: object.NewInteger(2),
},
// hash assignment with invalid index
{
env: prefilledEnv(map[string]object.Object{
"h": object.NewHash(map[object.HashKey]object.HashPair{
object.NewString("a").HashKey(): object.HashPair{
Key: object.NewString("a"),
Value: object.NewInteger(1),
},
}),
}),
a: newAstAssign(
&ast.Index{
Left: &ast.Identifier{Value: "h"},
Index: &ast.Infix{
Left: &ast.String{Value: "abc"},
Operator: "+",
Right: &ast.Integer{Value: 123},
},
},
&ast.Integer{Value: 2},
),
expected: object.NewErrorFormat("expected index to be hashable"),
},

// valid string assignment with positive index
{
env: prefilledEnv(map[string]object.Object{
"s": object.NewString("abc"),
}),
a: newAstAssign(
&ast.Index{
Left: &ast.Identifier{Value: "s"},
Index: &ast.Integer{Value: 0},
},
&ast.String{Value: "A"},
),
expected: object.NewString("A"),
},
// valid string assignment with negative index
{
env: prefilledEnv(map[string]object.Object{
"s": object.NewString("abc"),
}),
a: newAstAssign(
&ast.Index{
Left: &ast.Identifier{Value: "s"},
Index: &ast.Integer{Value: -1},
},
&ast.String{Value: "C"},
),
expected: object.NewString("C"),
},
// string assignment with non integer index
{
env: prefilledEnv(map[string]object.Object{
"s": object.NewString("abc"),
}),
a: newAstAssign(
&ast.Index{
Left: &ast.Identifier{Value: "s"},
Index: &ast.String{Value: "1"},
},
&ast.String{Value: "C"},
),
expected: object.NewErrorFormat("expected index to be an INTEGER, got STRING"),
},
// string assignment with integer index bigger than string size
{
env: prefilledEnv(map[string]object.Object{
"s": object.NewString("abc"),
}),
a: newAstAssign(
&ast.Index{
Left: &ast.Identifier{Value: "s"},
Index: &ast.Integer{Value: 3},
},
&ast.String{Value: "C"},
),
expected: object.NewErrorFormat("index out of range, got 3 but string is only 3 long"),
},
// string assignment with valid index but invalid value type
{
env: prefilledEnv(map[string]object.Object{
"s": object.NewString("abc"),
}),
a: newAstAssign(
&ast.Index{
Left: &ast.Identifier{Value: "s"},
Index: &ast.Integer{Value: 0},
},
&ast.Integer{Value: 1},
),
expected: object.NewErrorFormat("expected STRING object, got INTEGER"),
},
// string assignment with valid index but invalid value size
{
env: prefilledEnv(map[string]object.Object{
"s": object.NewString("abc"),
}),
a: newAstAssign(
&ast.Index{
Left: &ast.Identifier{Value: "s"},
Index: &ast.Integer{Value: 0},
},
&ast.String{Value: "AA"},
),
expected: object.NewErrorFormat("expected STRING object to have a length of 1, got 2"),
},
// index assignment with non indexable object
{
env: prefilledEnv(map[string]object.Object{
"i": object.NewInteger(123),
}),
a: newAstAssign(
&ast.Index{
Left: &ast.Identifier{Value: "i"},
Index: &ast.Integer{Value: 0},
},
&ast.Integer{Value: 4},
),
expected: object.NewErrorFormat("expected object to be indexable"),
},
}

for _, tc := range testcases {
if tc.env == nil {
tc.env = object.NewEnvironment()
}
obj := evalAssign(tc.a, tc.env)
if obj.Type() != tc.expected.Type() {
t.Errorf("expected object to be a %s, got %s", tc.expected.Type(), obj.Type())
continue
}
if obj.Inspect() != tc.expected.Inspect() {
t.Errorf("unexpected result, got=%s, want=%s", obj.Inspect(), tc.expected.Inspect())
continue
}
}
}
2 changes: 1 addition & 1 deletion evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
if object.IsError(index) {
return index
}
return evalIndex(left, index)
return evalIndex(left, index, node.Colon)

case *ast.ObjectCall:
res := evalObjectCall(node, env)
Expand Down
Loading

0 comments on commit 170e6a2

Please sign in to comment.