Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/parse/asp/equality_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package asp

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestEqualsFrozen(t *testing.T) {
l := pyList{pyString("a"), pyDict{"b": pyList{pyInt(1)}}}
d := pyDict{"x": l, "y": None}

assert.True(t, l.Equals(l.Freeze()))
assert.True(t, l.Freeze().Equals(l))
assert.True(t, d.Equals(d.Freeze()))
assert.True(t, d.Freeze().Equals(d))
assert.True(t, (pyList{}).Equals(pyList(nil)))
assert.True(t, (pyList{}).Equals(pyFrozenList{}))
assert.True(t, (pyDict{}).Equals(pyFrozenDict{}))
Comment thread
peterebden marked this conversation as resolved.
Resolved

// Deep equality of independently constructed nested structures
assert.True(t, (pyList{pyDict{"b": pyList{pyInt(1)}}}).Equals(pyList{pyDict{"b": pyList{pyInt(1)}}}))
assert.True(t, (pyDict{"a": pyList{pyDict{"b": None}}}).Equals(pyDict{"a": pyList{pyDict{"b": None}}.Freeze()}))
assert.False(t, (pyList{pyDict{"b": pyList{pyInt(1)}}}).Equals(pyList{pyDict{"b": pyList{pyInt(2)}}}))
assert.False(t, (pyDict{"a": pyList{pyDict{"b": None}}}).Equals(pyDict{"a": pyList{pyDict{"c": None}}}))

assert.False(t, l.Equals(l[:1].Freeze()))
assert.False(t, d.Equals(pyDict{"x": l}.Freeze()))
assert.False(t, d.Equals(pyDict{"x": l, "z": None}))
assert.False(t, (pyList{pyInt(1)}).Equals(pyList{pyString("1")}))
assert.False(t, (pyDict{}).Equals(pyList{}))
assert.False(t, (pyInt(1)).Equals(pyString("1")))
Comment thread
peterebden marked this conversation as resolved.
Resolved
}

func TestEqualsConfig(t *testing.T) {
c1 := &pyConfig{base: &pyConfigBase{dict: pyDict{"A": pyList{pyString("x")}}}, overlay: pyDict{"B": pyInt(1)}}
c2 := &pyConfig{base: &pyConfigBase{dict: pyDict{"A": pyList{pyString("x")}.Freeze()}}, overlay: pyDict{"B": pyInt(1)}}
c3 := &pyConfig{base: &pyConfigBase{dict: pyDict{"A": pyList{pyString("y")}}}, overlay: pyDict{"B": pyInt(1)}}

assert.True(t, c1.Equals(c1))
assert.True(t, c1.Equals(c2), "configs differing only in frozenness of nested values should be equal")
assert.True(t, c2.Equals(c1))
assert.False(t, c1.Equals(c3))
assert.False(t, c1.Equals(pyDict{"A": pyList{pyString("x")}, "B": pyInt(1)}))
}
5 changes: 2 additions & 3 deletions src/parse/asp/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"iter"
"path"
"path/filepath"
"reflect"
"regexp"
"runtime/pprof"
"strings"
Expand Down Expand Up @@ -726,9 +725,9 @@ func (s *scope) interpretOp(obj pyObject, op OpExpression) pyObject {
case Not:
return s.negate(obj)
case Equal:
return newPyBool(reflect.DeepEqual(obj, s.interpretExpression(op.Expr)))
return newPyBool(obj.Equals(s.interpretExpression(op.Expr)))
case NotEqual:
return newPyBool(!reflect.DeepEqual(obj, s.interpretExpression(op.Expr)))
return newPyBool(!obj.Equals(s.interpretExpression(op.Expr)))
case Is:
return s.interpretIs(obj, op)
case IsNot:
Expand Down
98 changes: 98 additions & 0 deletions src/parse/asp/objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ type pyObject interface {
TypeTag() int32
// Returns true if this object evaluates to something truthy.
IsTruthy() bool
// Returns true if this object is equal to the given one (i.e. implements the == operator).
Equals(other pyObject) bool
}

// A freezable represents an object that can be frozen into a readonly state.
Expand Down Expand Up @@ -94,6 +96,11 @@ func (b pyBool) IsTruthy() bool {
return b == True
}

func (b pyBool) Equals(other pyObject) bool {
o, ok := other.(pyBool)
return ok && b == o
}

func (b pyBool) String() string {
if b == True {
return "True"
Expand Down Expand Up @@ -125,6 +132,11 @@ func (n pyNone) IsTruthy() bool {
return false
}

func (n pyNone) Equals(other pyObject) bool {
_, ok := other.(pyNone)
return ok
}

func (n pyNone) String() string {
return "None"
}
Expand Down Expand Up @@ -155,6 +167,11 @@ func (s pySentinel) IsTruthy() bool {
return false
}

func (s pySentinel) Equals(other pyObject) bool {
o, ok := other.(pySentinel)
return ok && s == o
}

func (s pySentinel) String() string {
panic("non stringable type " + string(s))
}
Expand Down Expand Up @@ -210,6 +227,11 @@ func (i pyInt) IsTruthy() bool {
return i != 0
}

func (i pyInt) Equals(other pyObject) bool {
o, ok := other.(pyInt)
return ok && i == o
}

func (i pyInt) Operator(operator Operator, operand pyObject) pyObject {
switch o := operand.(type) {
case pyInt:
Expand Down Expand Up @@ -268,6 +290,11 @@ func (s pyString) IsTruthy() bool {
return s != ""
}

func (s pyString) Equals(other pyObject) bool {
o, ok := other.(pyString)
return ok && s == o
}

func (s pyString) Property(scope *scope, name string) pyObject {
if prop, present := scope.interpreter.stringMethods[name]; present {
return prop.Member(s)
Expand Down Expand Up @@ -349,6 +376,29 @@ func (l pyList) IsTruthy() bool {
return len(l) > 0
}

// Equals compares this list to another, element by element. Frozen lists compare equal to unfrozen
// ones with the same contents.
func (l pyList) Equals(other pyObject) bool {
var o pyList
switch other := other.(type) {
case pyList:
o = other
case pyFrozenList:
o = other.pyList
default:
return false
}
if len(l) != len(o) {
return false
}
for i, v := range l {
if !v.Equals(o[i]) {
return false
}
}
return true
}

func (l pyList) Operator(operator Operator, operand pyObject) pyObject {
switch operator {
case Add:
Expand Down Expand Up @@ -483,6 +533,29 @@ func (d pyDict) IsTruthy() bool {
return len(d) > 0
}

// Equals compares this dict to another. Frozen dicts compare equal to unfrozen ones with the
// same contents.
func (d pyDict) Equals(other pyObject) bool {
var o pyDict
switch other := other.(type) {
case pyDict:
o = other
case pyFrozenDict:
o = other.pyDict
default:
return false
}
if len(d) != len(o) {
return false
}
for k, v := range d {
if v2, present := o[k]; !present || !v.Equals(v2) {
return false
}
}
return true
}

func (d pyDict) Property(scope *scope, name string) pyObject {
// We allow looking up dict members by . as well as by indexing in order to facilitate the config map.
if obj, present := d[name]; present {
Expand Down Expand Up @@ -687,6 +760,11 @@ func (f *pyFunc) IsTruthy() bool {
return true
}

func (f *pyFunc) Equals(other pyObject) bool {
o, ok := other.(*pyFunc)
return ok && f == o
}

func (f *pyFunc) String() string {
return fmt.Sprintf("<function %s>", f.name)
}
Expand Down Expand Up @@ -916,6 +994,21 @@ func (c *pyConfig) IsTruthy() bool {
return true // sure, why not
}

// Equals compares this config to another. Frozen configs compare equal to unfrozen ones with the
// same contents.
func (c *pyConfig) Equals(other pyObject) bool {
var o *pyConfig
switch other := other.(type) {
case *pyConfig:
o = other
case *pyFrozenConfig:
o = &other.pyConfig
default:
return false
}
return c == o || (c.base.dict.Equals(o.base.dict) && c.overlay.Equals(o.overlay))
}

func (c *pyConfig) Property(scope *scope, name string) pyObject {
if obj := c.Get(name, nil); obj != nil {
return obj
Expand Down Expand Up @@ -1057,6 +1150,11 @@ func (r *pyRange) IsTruthy() bool {
return true
}

func (r *pyRange) Equals(other pyObject) bool {
o, ok := other.(*pyRange)
return ok && *r == *o
}

func (r *pyRange) Operator(operator Operator, operand pyObject) pyObject {
if l, ok := operand.(pyList); ok && operator == Add {
return append(r.toList(len(l)), l...)
Expand Down
Loading