Merge pull request #412 from nspcc-dev/feature/map

VM: implement maps, closes #359.
This commit is contained in:
Roman Khimov 2019-09-25 19:00:26 +03:00 committed by GitHub
commit ab8d9c59d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 452 additions and 24 deletions

View file

@ -19,10 +19,18 @@ func makeStackItem(v interface{}) StackItem {
return &BigIntegerItem{
value: big.NewInt(int64(val)),
}
case int64:
return &BigIntegerItem{
value: big.NewInt(val),
}
case []byte:
return &ByteArrayItem{
value: val,
}
case string:
return &ByteArrayItem{
value: []byte(val),
}
case bool:
return &BoolItem{
value: val,
@ -193,3 +201,50 @@ func (i *ArrayItem) MarshalJSON() ([]byte, error) {
func (i *ArrayItem) String() string {
return "Array"
}
// MapItem represents Map object.
type MapItem struct {
value map[interface{}]StackItem
}
// NewMapItem returns new MapItem object.
func NewMapItem() *MapItem {
return &MapItem{
value: make(map[interface{}]StackItem),
}
}
// Value implements StackItem interface.
func (i *MapItem) Value() interface{} {
return i.value
}
// MarshalJSON implements the json.Marshaler interface.
func (i *MapItem) String() string {
return "Map"
}
// Has checks if map has specified key.
func (i *MapItem) Has(key StackItem) (ok bool) {
_, ok = i.value[toMapKey(key)]
return
}
// Add adds key-value pair to the map.
func (i *MapItem) Add(key, value StackItem) {
i.value[toMapKey(key)] = value
}
// toMapKey converts StackItem so that it can be used as a map key.
func toMapKey(key StackItem) interface{} {
switch t := key.(type) {
case *BoolItem:
return t.value
case *BigIntegerItem:
return t.value.Int64()
case *ByteArrayItem:
return string(t.value)
default:
panic("wrong key type")
}
}

View file

@ -478,6 +478,11 @@ func (v *VM) execute(ctx *Context, op Instruction) {
v.estack.PushVal(ta == tb)
break
}
} else if ma, ok := a.value.(*MapItem); ok {
if mb, ok := b.value.(*MapItem); ok {
v.estack.PushVal(ma == mb)
break
}
}
v.estack.PushVal(reflect.DeepEqual(a, b))
@ -665,10 +670,7 @@ func (v *VM) execute(ctx *Context, op Instruction) {
itemElem := v.estack.Pop()
arrElem := v.estack.Pop()
val := itemElem.value
if t, ok := itemElem.value.(*StructItem); ok {
val = t.Clone()
}
val := cloneIfStruct(itemElem.value)
switch t := arrElem.value.(type) {
case *ArrayItem:
@ -705,11 +707,11 @@ func (v *VM) execute(ctx *Context, op Instruction) {
v.estack.PushVal(l)
case PICKITEM:
var (
key = v.estack.Pop()
obj = v.estack.Pop()
index = int(key.BigInt().Int64())
)
key := v.estack.Pop()
validateMapKey(key)
obj := v.estack.Pop()
index := int(key.BigInt().Int64())
switch t := obj.value.(type) {
// Struct and Array items have their underlying value as []StackItem.
@ -720,6 +722,12 @@ func (v *VM) execute(ctx *Context, op Instruction) {
}
item := arr[index]
v.estack.PushVal(item)
case *MapItem:
if !t.Has(key.value) {
panic("invalid key")
}
k := toMapKey(key.value)
v.estack.Push(&Element{value: t.value[k]})
default:
arr := obj.Bytes()
if index < 0 || index >= len(arr) {
@ -730,21 +738,24 @@ func (v *VM) execute(ctx *Context, op Instruction) {
}
case SETITEM:
var (
item = v.estack.Pop().value
key = v.estack.Pop()
obj = v.estack.Pop()
index = int(key.BigInt().Int64())
)
item := v.estack.Pop().value
key := v.estack.Pop()
validateMapKey(key)
obj := v.estack.Pop()
switch t := obj.value.(type) {
// Struct and Array items have their underlying value as []StackItem.
case *ArrayItem, *StructItem:
arr := t.Value().([]StackItem)
index := int(key.BigInt().Int64())
if index < 0 || index >= len(arr) {
panic("SETITEM: invalid index")
}
arr[index] = item
case *MapItem:
t.Add(key.value, item)
default:
panic(fmt.Sprintf("SETITEM: invalid item type %s", t))
}
@ -757,23 +768,31 @@ func (v *VM) execute(ctx *Context, op Instruction) {
}
}
case REMOVE:
key := int(v.estack.Pop().BigInt().Int64())
key := v.estack.Pop()
validateMapKey(key)
elem := v.estack.Pop()
switch t := elem.value.(type) {
case *ArrayItem:
a := t.value
if key < 0 || key >= len(a) {
k := int(key.BigInt().Int64())
if k < 0 || k >= len(a) {
panic("REMOVE: invalid index")
}
a = append(a[:key], a[key+1:]...)
a = append(a[:k], a[k+1:]...)
t.value = a
case *StructItem:
a := t.value
if key < 0 || key >= len(a) {
k := int(key.BigInt().Int64())
if k < 0 || k >= len(a) {
panic("REMOVE: invalid index")
}
a = append(a[:key], a[key+1:]...)
a = append(a[:k], a[k+1:]...)
t.value = a
case *MapItem:
m := t.value
k := toMapKey(key.value)
delete(m, k)
default:
panic("REMOVE: invalid type")
}
@ -785,10 +804,10 @@ func (v *VM) execute(ctx *Context, op Instruction) {
switch t := elem.value.Value().(type) {
case []StackItem:
v.estack.PushVal(len(t))
case []uint8:
case map[interface{}]StackItem:
v.estack.PushVal(len(t))
default:
panic("ARRAYSIZE: item not of type []StackItem")
v.estack.PushVal(len(elem.Bytes()))
}
case SIZE:
@ -912,8 +931,71 @@ func (v *VM) execute(ctx *Context, op Instruction) {
}
v.estack.PushVal(sigok)
case NEWMAP, HASKEY, KEYS, VALUES:
panic("unimplemented")
case NEWMAP:
v.estack.Push(&Element{value: NewMapItem()})
case KEYS:
item := v.estack.Pop()
if item == nil {
panic("no argument")
}
m, ok := item.value.(*MapItem)
if !ok {
panic("not a Map")
}
arr := make([]StackItem, 0, len(m.value))
for k := range m.value {
arr = append(arr, makeStackItem(k))
}
v.estack.PushVal(arr)
case VALUES:
item := v.estack.Pop()
if item == nil {
panic("no argument")
}
var arr []StackItem
switch t := item.value.(type) {
case *ArrayItem, *StructItem:
src := t.Value().([]StackItem)
arr = make([]StackItem, len(src))
for i := range src {
arr[i] = cloneIfStruct(src[i])
}
case *MapItem:
arr = make([]StackItem, 0, len(t.value))
for k := range t.value {
arr = append(arr, cloneIfStruct(t.value[k]))
}
default:
panic("not a Map, Array or Struct")
}
v.estack.PushVal(arr)
case HASKEY:
key := v.estack.Pop()
validateMapKey(key)
c := v.estack.Pop()
if c == nil {
panic("no value found")
}
switch t := c.value.(type) {
case *ArrayItem, *StructItem:
index := key.BigInt().Int64()
if index < 0 {
panic("negative index")
}
v.estack.PushVal(index < int64(len(c.Array())))
case *MapItem:
v.estack.PushVal(t.Has(key.value))
default:
panic("wrong collection type")
}
// Cryptographic operations.
case SHA1:
@ -950,6 +1032,15 @@ func (v *VM) execute(ctx *Context, op Instruction) {
}
}
func cloneIfStruct(item StackItem) StackItem {
switch it := item.(type) {
case *StructItem:
return it.Clone()
default:
return it
}
}
func makeArrayOfFalses(n int) []StackItem {
items := make([]StackItem, n)
for i := range items {
@ -958,6 +1049,16 @@ func makeArrayOfFalses(n int) []StackItem {
return items
}
func validateMapKey(key *Element) {
if key == nil {
panic("no key found")
}
switch key.value.(type) {
case *ArrayItem, *StructItem, *MapItem:
panic("key can't be a collection")
}
}
func init() {
log.SetPrefix("NEO-GO-VM > ")
log.SetFlags(0)

View file

@ -446,6 +446,27 @@ func TestEQUALArrayFalse(t *testing.T) {
assert.Equal(t, &BoolItem{false}, vm.estack.Pop().value)
}
func TestEQUALMapTrue(t *testing.T) {
prog := makeProgram(DUP, EQUAL)
vm := load(prog)
vm.estack.Push(&Element{value: NewMapItem()})
vm.Run()
assert.Equal(t, false, vm.HasFailed())
assert.Equal(t, 1, vm.estack.Len())
assert.Equal(t, &BoolItem{true}, vm.estack.Pop().value)
}
func TestEQUALMapFalse(t *testing.T) {
prog := makeProgram(EQUAL)
vm := load(prog)
vm.estack.Push(&Element{value: NewMapItem()})
vm.estack.Push(&Element{value: NewMapItem()})
vm.Run()
assert.Equal(t, false, vm.HasFailed())
assert.Equal(t, 1, vm.estack.Len())
assert.Equal(t, &BoolItem{false}, vm.estack.Pop().value)
}
func TestNumEqual(t *testing.T) {
prog := makeProgram(NUMEQUAL)
vm := load(prog)
@ -646,6 +667,39 @@ func TestPICKITEMByteArray(t *testing.T) {
assert.Equal(t, makeStackItem(2), vm.estack.Pop().value)
}
func TestPICKITEMMap(t *testing.T) {
prog := makeProgram(PICKITEM)
vm := load(prog)
m := NewMapItem()
m.Add(makeStackItem(5), makeStackItem(3))
vm.estack.Push(&Element{value: m})
vm.estack.PushVal(makeStackItem(5))
vm.Run()
assert.Equal(t, false, vm.HasFailed())
assert.Equal(t, 1, vm.estack.Len())
assert.Equal(t, makeStackItem(3), vm.estack.Pop().value)
}
func TestSETITEMMap(t *testing.T) {
prog := makeProgram(SETITEM, PICKITEM)
vm := load(prog)
m := NewMapItem()
m.Add(makeStackItem(5), makeStackItem(3))
vm.estack.Push(&Element{value: m})
vm.estack.PushVal(5)
vm.estack.Push(&Element{value: m})
vm.estack.PushVal(5)
vm.estack.PushVal([]byte{0, 1})
vm.Run()
assert.Equal(t, false, vm.HasFailed())
assert.Equal(t, 1, vm.estack.Len())
assert.Equal(t, makeStackItem([]byte{0, 1}), vm.estack.Pop().value)
}
func TestSIZENoArgument(t *testing.T) {
prog := makeProgram(SIZE)
vm := load(prog)
@ -673,6 +727,207 @@ func TestSIZEBool(t *testing.T) {
assert.Equal(t, makeStackItem(1), vm.estack.Pop().value)
}
func TestARRAYSIZEArray(t *testing.T) {
prog := makeProgram(ARRAYSIZE)
vm := load(prog)
vm.estack.PushVal([]StackItem{
makeStackItem(1),
makeStackItem([]byte{}),
})
vm.Run()
assert.Equal(t, false, vm.HasFailed())
assert.Equal(t, 1, vm.estack.Len())
assert.Equal(t, makeStackItem(2), vm.estack.Pop().value)
}
func TestARRAYSIZEMap(t *testing.T) {
prog := makeProgram(ARRAYSIZE)
vm := load(prog)
m := NewMapItem()
m.Add(makeStackItem(5), makeStackItem(6))
m.Add(makeStackItem([]byte{0, 1}), makeStackItem(6))
vm.estack.Push(&Element{value: m})
vm.Run()
assert.Equal(t, false, vm.HasFailed())
assert.Equal(t, 1, vm.estack.Len())
assert.Equal(t, makeStackItem(2), vm.estack.Pop().value)
}
func TestKEYSMap(t *testing.T) {
prog := makeProgram(KEYS)
vm := load(prog)
m := NewMapItem()
m.Add(makeStackItem(5), makeStackItem(6))
m.Add(makeStackItem([]byte{0, 1}), makeStackItem(6))
vm.estack.Push(&Element{value: m})
vm.Run()
assert.Equal(t, false, vm.HasFailed())
assert.Equal(t, 1, vm.estack.Len())
top := vm.estack.Pop().value.(*ArrayItem)
assert.Equal(t, 2, len(top.value))
assert.Contains(t, top.value, makeStackItem(5))
assert.Contains(t, top.value, makeStackItem([]byte{0, 1}))
}
func TestKEYSNoArgument(t *testing.T) {
prog := makeProgram(KEYS)
vm := load(prog)
vm.Run()
assert.Equal(t, true, vm.HasFailed())
}
func TestKEYSWrongType(t *testing.T) {
prog := makeProgram(KEYS)
vm := load(prog)
vm.estack.PushVal([]StackItem{})
vm.Run()
assert.Equal(t, true, vm.HasFailed())
}
func TestVALUESMap(t *testing.T) {
prog := makeProgram(VALUES)
vm := load(prog)
m := NewMapItem()
m.Add(makeStackItem(5), makeStackItem([]byte{2, 3}))
m.Add(makeStackItem([]byte{0, 1}), makeStackItem([]StackItem{}))
vm.estack.Push(&Element{value: m})
vm.Run()
assert.Equal(t, false, vm.HasFailed())
assert.Equal(t, 1, vm.estack.Len())
top := vm.estack.Pop().value.(*ArrayItem)
assert.Equal(t, 2, len(top.value))
assert.Contains(t, top.value, makeStackItem([]byte{2, 3}))
assert.Contains(t, top.value, makeStackItem([]StackItem{}))
}
func TestVALUESArray(t *testing.T) {
prog := makeProgram(VALUES)
vm := load(prog)
vm.estack.PushVal([]StackItem{makeStackItem(4)})
vm.Run()
assert.Equal(t, false, vm.HasFailed())
assert.Equal(t, 1, vm.estack.Len())
assert.Equal(t, &ArrayItem{[]StackItem{makeStackItem(4)}}, vm.estack.Pop().value)
}
func TestVALUESNoArgument(t *testing.T) {
prog := makeProgram(VALUES)
vm := load(prog)
vm.Run()
assert.Equal(t, true, vm.HasFailed())
}
func TestVALUESWrongType(t *testing.T) {
prog := makeProgram(VALUES)
vm := load(prog)
vm.estack.PushVal(5)
vm.Run()
assert.Equal(t, true, vm.HasFailed())
}
func TestHASKEYArrayTrue(t *testing.T) {
prog := makeProgram(PUSH5, NEWARRAY, PUSH4, HASKEY)
vm := load(prog)
vm.Run()
assert.Equal(t, false, vm.HasFailed())
assert.Equal(t, 1, vm.estack.Len())
assert.Equal(t, makeStackItem(true), vm.estack.Pop().value)
}
func TestHASKEYArrayFalse(t *testing.T) {
prog := makeProgram(PUSH5, NEWARRAY, PUSH5, HASKEY)
vm := load(prog)
vm.Run()
assert.Equal(t, false, vm.HasFailed())
assert.Equal(t, 1, vm.estack.Len())
assert.Equal(t, makeStackItem(false), vm.estack.Pop().value)
}
func TestHASKEYStructTrue(t *testing.T) {
prog := makeProgram(PUSH5, NEWSTRUCT, PUSH4, HASKEY)
vm := load(prog)
vm.Run()
assert.Equal(t, false, vm.HasFailed())
assert.Equal(t, 1, vm.estack.Len())
assert.Equal(t, makeStackItem(true), vm.estack.Pop().value)
}
func TestHASKEYStructFalse(t *testing.T) {
prog := makeProgram(PUSH5, NEWSTRUCT, PUSH5, HASKEY)
vm := load(prog)
vm.Run()
assert.Equal(t, false, vm.HasFailed())
assert.Equal(t, 1, vm.estack.Len())
assert.Equal(t, makeStackItem(false), vm.estack.Pop().value)
}
func TestHASKEYMapTrue(t *testing.T) {
prog := makeProgram(HASKEY)
vm := load(prog)
m := NewMapItem()
m.Add(makeStackItem(5), makeStackItem(6))
vm.estack.Push(&Element{value: m})
vm.estack.PushVal(5)
vm.Run()
assert.Equal(t, false, vm.HasFailed())
assert.Equal(t, 1, vm.estack.Len())
assert.Equal(t, makeStackItem(true), vm.estack.Pop().value)
}
func TestHASKEYMapFalse(t *testing.T) {
prog := makeProgram(HASKEY)
vm := load(prog)
m := NewMapItem()
m.Add(makeStackItem(5), makeStackItem(6))
vm.estack.Push(&Element{value: m})
vm.estack.PushVal(6)
vm.Run()
assert.Equal(t, false, vm.HasFailed())
assert.Equal(t, 1, vm.estack.Len())
assert.Equal(t, makeStackItem(false), vm.estack.Pop().value)
}
func TestHASKEYNoArguments(t *testing.T) {
prog := makeProgram(HASKEY)
vm := load(prog)
vm.Run()
assert.Equal(t, true, vm.HasFailed())
}
func TestHASKEY1Argument(t *testing.T) {
prog := makeProgram(HASKEY)
vm := load(prog)
vm.estack.PushVal(1)
vm.Run()
assert.Equal(t, true, vm.HasFailed())
}
func TestHASKEYWrongKeyType(t *testing.T) {
prog := makeProgram(HASKEY)
vm := load(prog)
vm.estack.PushVal([]StackItem{})
vm.estack.PushVal([]StackItem{})
vm.Run()
assert.Equal(t, true, vm.HasFailed())
}
func TestHASKEYWrongCollectionType(t *testing.T) {
prog := makeProgram(HASKEY)
vm := load(prog)
vm.estack.PushVal(1)
vm.estack.PushVal(2)
vm.Run()
assert.Equal(t, true, vm.HasFailed())
}
func TestSIGNNoArgument(t *testing.T) {
prog := makeProgram(SIGN)
vm := load(prog)
@ -1490,6 +1745,23 @@ func TestREMOVEGood(t *testing.T) {
assert.Equal(t, makeStackItem(1), vm.estack.Pop().value)
}
func TestREMOVEMap(t *testing.T) {
prog := makeProgram(REMOVE, PUSH5, HASKEY)
vm := load(prog)
m := NewMapItem()
m.Add(makeStackItem(5), makeStackItem(3))
m.Add(makeStackItem([]byte{0, 1}), makeStackItem([]byte{2, 3}))
vm.estack.Push(&Element{value: m})
vm.estack.Push(&Element{value: m})
vm.estack.PushVal(makeStackItem(5))
vm.Run()
assert.Equal(t, false, vm.HasFailed())
assert.Equal(t, 1, vm.estack.Len())
assert.Equal(t, makeStackItem(false), vm.estack.Pop().value)
}
func TestCHECKSIGNoArgs(t *testing.T) {
prog := makeProgram(CHECKSIG)
vm := load(prog)