diff --git a/src/parse/asp/equality_test.go b/src/parse/asp/equality_test.go new file mode 100644 index 000000000..01ff825e6 --- /dev/null +++ b/src/parse/asp/equality_test.go @@ -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{})) + + // 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"))) +} + +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)})) +} diff --git a/src/parse/asp/interpreter.go b/src/parse/asp/interpreter.go index 2f1373ea1..10eda42ab 100644 --- a/src/parse/asp/interpreter.go +++ b/src/parse/asp/interpreter.go @@ -6,7 +6,6 @@ import ( "iter" "path" "path/filepath" - "reflect" "regexp" "runtime/pprof" "strings" @@ -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: diff --git a/src/parse/asp/objects.go b/src/parse/asp/objects.go index cd23bb3d2..d49e52341 100644 --- a/src/parse/asp/objects.go +++ b/src/parse/asp/objects.go @@ -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. @@ -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" @@ -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" } @@ -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)) } @@ -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: @@ -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) @@ -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: @@ -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 { @@ -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("", f.name) } @@ -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 @@ -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...)