diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index 0496d37ed72..156ac133df6 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -165,7 +165,7 @@ type Attributes struct { Line int Column int Label Name - data map[GnoAttribute]interface{} // not persisted + data []*attrKV // not persisted } func (attr *Attributes) GetLine() int { @@ -193,28 +193,62 @@ func (attr *Attributes) SetLabel(label Name) { } func (attr *Attributes) HasAttribute(key GnoAttribute) bool { - _, ok := attr.data[key] + _, _, ok := attr.getAttribute(key) return ok } // GnoAttribute must not be user provided / arbitrary, // otherwise will create potential exploits. func (attr *Attributes) GetAttribute(key GnoAttribute) interface{} { - return attr.data[key] + val, _, _ := attr.getAttribute(key) + return val +} + +func (attr *Attributes) getAttribute(key GnoAttribute) (any, int, bool) { + for i, kv := range attr.data { + if kv.key == key { + return kv.value, i, true + } + } + return nil, -1, false +} + +type attrKV struct { + key GnoAttribute + value any } func (attr *Attributes) SetAttribute(key GnoAttribute, value interface{}) { if attr.data == nil { - attr.data = make(map[GnoAttribute]interface{}) + attr.data = make([]*attrKV, 0, 4) } - attr.data[key] = value + + for _, kv := range attr.data { + if kv.key == key { + kv.value = value + return + } + } + + attr.data = append(attr.data, &attrKV{key, value}) } func (attr *Attributes) DelAttribute(key GnoAttribute) { if debug && attr.data == nil { panic("should not happen, attribute is expected to be non-empty.") } - delete(attr.data, key) + _, index, _ := attr.getAttribute(key) + if index < 0 { + return + } + + if index == 0 { + attr.data = attr.data[1:] + } else if index == len(attr.data)-1 { + attr.data = attr.data[:len(attr.data)-1] + } else { + attr.data = append(attr.data[:index], attr.data[index+1:]...) + } } // ---------------------------------------- diff --git a/gnovm/pkg/gnolang/nodes_test.go b/gnovm/pkg/gnolang/nodes_test.go index 2c3a03d8c09..9f057482324 100644 --- a/gnovm/pkg/gnolang/nodes_test.go +++ b/gnovm/pkg/gnolang/nodes_test.go @@ -1,6 +1,7 @@ package gnolang_test import ( + "fmt" "math" "testing" @@ -42,3 +43,59 @@ func TestStaticBlock_Define2_MaxNames(t *testing.T) { // This one should panic because the maximum number of names has been reached. staticBlock.Define2(false, gnolang.Name("a"), gnolang.BoolType, gnolang.TypedValue{T: gnolang.BoolType}) } + +func TestAttributesSetGetDel(t *testing.T) { + attrs := new(gnolang.Attributes) + if got, want := attrs.GetAttribute("a"), (any)(nil); got != want { + t.Errorf(".Get returned an unexpected value=%v, want=%v", got, want) + } + attrs.SetAttribute("a", 10) + if got, want := attrs.GetAttribute("a"), 10; got != want { + t.Errorf(".Get returned an unexpected value=%v, want=%v", got, want) + } + attrs.SetAttribute("a", 20) + if got, want := attrs.GetAttribute("a"), 20; got != want { + t.Errorf(".Get returned an unexpected value=%v, want=%v", got, want) + } + attrs.DelAttribute("a") + if got, want := attrs.GetAttribute("a"), (any)(nil); got != want { + t.Errorf(".Get returned an unexpected value=%v, want=%v", got, want) + } +} + +var sink any = nil + +func BenchmarkAttributesSetGetDel(b *testing.B) { + n := 100 + keys := make([]gnolang.GnoAttribute, 0, n) + for i := 0; i < n; i++ { + keys = append(keys, gnolang.GnoAttribute(fmt.Sprintf("%d", i))) + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + attrs := new(gnolang.Attributes) + for j := 0; j < 100; j++ { + sink = attrs.GetAttribute("a") + } + for j := 0; j < 100; j++ { + attrs.SetAttribute("a", j) + sink = attrs.GetAttribute("a") + } + + for j, key := range keys { + attrs.SetAttribute(key, j) + } + + for _, key := range keys { + sink = attrs.GetAttribute(key) + attrs.GetAttribute(key) + } + } + + if sink == nil { + b.Fatal("Benchmark did not run!") + } +}