Commit 24d08dca authored by Meador Inge's avatar Meador Inge Committed by Dylan Trotter

Add support for the `cmp` builtin (#54) (#63) (#80)

Implement the `cmp` as defined here:

* https://docs.python.org/2/library/functions.html#cmp

Implementing this builtin required implementing a fair
amount of supporting logic to do 3-way comparisons. The
3-way comparison implementation in CPython is somewhat
complex, thus there may be some cases that still need
support. This is a good start, though.
parent b730a445
......@@ -266,6 +266,13 @@ func builtinChr(f *Frame, args Args, _ KWArgs) (*Object, *BaseException) {
return NewStr(string([]byte{byte(i)})).ToObject(), nil
}
func builtinCmp(f *Frame, args Args, _ KWArgs) (*Object, *BaseException) {
if raised := checkFunctionArgs(f, "cmp", args, ObjectType, ObjectType); raised != nil {
return nil, raised
}
return Compare(f, args[0], args[1])
}
func builtinDir(f *Frame, args Args, kwargs KWArgs) (*Object, *BaseException) {
// TODO: Support __dir__.
if raised := checkFunctionArgs(f, "dir", args, ObjectType); raised != nil {
......@@ -554,6 +561,7 @@ func init() {
"bin": newBuiltinFunction("bin", builtinBin).ToObject(),
"callable": newBuiltinFunction("callable", builtinCallable).ToObject(),
"chr": newBuiltinFunction("chr", builtinChr).ToObject(),
"cmp": newBuiltinFunction("cmp", builtinCmp).ToObject(),
"dir": newBuiltinFunction("dir", builtinDir).ToObject(),
"False": False.ToObject(),
"getattr": newBuiltinFunction("getattr", builtinGetAttr).ToObject(),
......
......@@ -65,6 +65,29 @@ func Assert(f *Frame, cond *Object, msg *Object) *BaseException {
return raised
}
// compare implements a 3-way comparison which returns:
//
// -1 if v < w
// 0 if v == w
// 1 if v > w
//
// It closely resembles the behavior of CPython's do_cmp in object.c.
func Compare(f *Frame, v, w *Object) (*Object, *BaseException) {
cmp := v.typ.slots.Cmp
if v.typ == w.typ && cmp != nil {
return cmp.Fn(f, v, w)
}
r, raised := tryRichTo3wayCompare(f, v, w)
if r != NotImplemented {
return r, raised
}
r, raised = try3wayCompare(f, v, w)
if r != NotImplemented {
return r, raised
}
return NewInt(compareDefault(f, v, w)).ToObject(), nil
}
// Contains checks whether value is present in seq. It first checks the
// __contains__ method of seq and, if that is not available, attempts to find
// value by iteration over seq. It is equivalent to the Python expression
......@@ -936,6 +959,83 @@ func compareDefault(f *Frame, v, w *Object) int {
return 1
}
// tryRichCompareBool tries a rich comparison with the given comparison op and
// returns a bool indicating if the relation is true. It closely resembles the
// behavior of CPython's try_rich_compare_bool in object.c.
func tryRichCompareBool(f *Frame, op compareOp, v, w *Object) (bool, *BaseException) {
r, raised := compareRich(f, op, v, w)
if raised != nil {
return false, raised
}
if r == NotImplemented {
return false, nil
}
br, raised := IsTrue(f, r)
if raised != nil {
return false, raised
}
return br, raised
}
// halfCompare tries a comparison with the __cmp__ slot, ensures the result
// is an integer, and returns it. It closely resembles the behavior of CPython's
// half_compare in typeobject.c.
func halfCompare(f *Frame, v, w *Object) (*Object, *BaseException) {
cmp := v.typ.slots.Cmp
r, raised := cmp.Fn(f, v, w)
if raised != nil {
return nil, raised
}
if !r.isInstance(IntType) {
return nil, f.RaiseType(TypeErrorType, "an integer is required")
}
return r, nil
}
// try3wayCompare tries a comparison with the __cmp__ slot with the given
// arguments. It first tries to use the __cmp__ slot on v and if that fails
// on w. It closely resembles the behavior of CPython's try_3way_compare in
// object.c.
func try3wayCompare(f *Frame, v, w *Object) (*Object, *BaseException) {
cmp := v.typ.slots.Cmp
if cmp != nil {
return halfCompare(f, v, w)
}
cmp = w.typ.slots.Cmp
if cmp != nil {
r, raised := halfCompare(f, w, v)
if raised != nil {
return nil, raised
}
return intNeg(f, r)
}
return NotImplemented, nil
}
// tryRichTo3wayCompare tries to compute a 3-way comparison in terms of
// the rich comparison operators (if they exist). It closely resembles
// the behavior of CPython's try_rich_to_3way_compare in object.c.
func tryRichTo3wayCompare(f *Frame, v, w *Object) (*Object, *BaseException) {
var tries = []struct {
op compareOp
outcome int
}{
{compareOpEq, 0},
{compareOpLT, -1},
{compareOpGT, 1},
}
for _, try := range tries {
r, raised := tryRichCompareBool(f, try.op, v, w)
if raised != nil {
return nil, raised
}
if r {
return NewInt(try.outcome).ToObject(), nil
}
}
return NotImplemented, nil
}
func checkFunctionArgs(f *Frame, function string, args Args, types ...*Type) *BaseException {
if len(args) != len(types) {
msg := fmt.Sprintf("'%s' requires %d arguments", function, len(types))
......
......@@ -374,6 +374,7 @@ type typeSlots struct {
And *binaryOpSlot
Basis *basisSlot
Call *callSlot
Cmp *binaryOpSlot
Contains *binaryOpSlot
DelAttr *delAttrSlot
Delete *deleteSlot
......
......@@ -101,3 +101,133 @@ class bar(object):
assert callable(bar)
assert callable(bar())
# cmp(x)
# Test simple cases.
assert cmp(1, 2) == -1
assert cmp(3, 3) == 0
assert cmp(5, 4) == 1
class Lt(object):
def __init__(self, x):
self.lt_called = False
self.x = x
def __lt__(self, other):
self.lt_called = True
return self.x < other.x
class Eq(object):
def __init__(self, x):
self.eq_called = False
self.x = x
def __eq__(self, other):
self.eq_called = True
return self.x == other.x
class Gt(object):
def __init__(self, x):
self.gt_called = False
self.x = x
def __gt__(self, other):
self.gt_called = True
return self.x > other.x
class RichCmp(Lt, Eq, Gt):
def __init__(self, x):
self.x = x
class Cmp(object):
def __init__(self, x):
self.cmp_called = False
self.x = x
def __cmp__(self, other):
self.cmp_called = True
if self.x < other.x:
return -1
elif self.x > other.x:
return 1
else:
return 0
class NoCmp(object):
def __init__(self, x):
self.x = x
# Test 3-way compare in terms of rich compare.
a, b = RichCmp(1), RichCmp(2)
assert cmp(a, b) == -1
assert a.lt_called
a, b = RichCmp(3), RichCmp(3)
assert cmp(a, b) == 0
assert a.eq_called
a, b = RichCmp(5), RichCmp(4)
assert cmp(a, b) == 1
assert a.gt_called
# Test pure 3-way compare.
a, b = Cmp(1), Cmp(2)
assert cmp(a, b) == -1
assert a.cmp_called
a, b = Cmp(3), Cmp(3)
assert cmp(a, b) == 0
assert a.cmp_called
a, b = Cmp(5), Cmp(4)
assert cmp(a, b) == 1
# Test mixed 3-way and rich compare.
a, b = RichCmp(1), Cmp(2)
assert cmp(a, b) == -1
assert a.lt_called
assert not b.cmp_called
a, b = Cmp(1), RichCmp(2)
assert cmp(a, b) == -1
assert not a.cmp_called
assert b.gt_called
a, b = RichCmp(3), Cmp(3)
assert cmp(a, b) == 0
assert a.eq_called
assert not b.cmp_called
a, b = Cmp(3), RichCmp(3)
assert cmp(a, b) == 0
assert not a.cmp_called
assert b.eq_called
a, b = RichCmp(5), Cmp(4)
assert cmp(a, b) == 1
assert a.gt_called
assert not b.cmp_called
a, b = Cmp(5), RichCmp(4)
assert cmp(a, b) == 1
assert not a.cmp_called
assert b.gt_called
# Test compare on only one object.
a, b = Cmp(1), NoCmp(2)
assert cmp(a, b) == -1
assert a.cmp_called
a, b = NoCmp(1), Cmp(2)
assert cmp(a, b) == -1
assert b.cmp_called
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment