Add builtin equals(); change ==, !=; add ===, !==

The equals() method is a builtin which exists on every object and value.
There are several implementations, which depend on the object's type:
boolean: true iff passed a boolean which is equivalent to this
integer: true iff passed an equivalent integer (of any integer type)
         Each integer's equals() checks for all other types, so e.g.
         short.equals(ulong) will be true if they are the same value
         under ===.
floating: true iff passed an equivalent float or double
          As with integers, both types' equals() methods check for both
          types, so float.equals(double) can return true.
object: true iff passed an object with an identical address (i.e. ===)
array: true iff passed an array with the same RTTI, the same length, and
       all of the elements compare equal with ==
function: true iff passed a function with the same RTTI, which compares
          equal with ==
tuple: true iff passed a tuple with the same RTTI, with elements that
       compare equal with ==

The == and != operators call the equals() method after performing any
necessary null checks. It is up to the implementation which way around
to call it, so:
x == y
may come out as:
x.equals(y)
or:
y.equals(x)
(though, it is still guaranteed that x will be evaluated before y)
This allows the compiler to optimise null checks like:
x == null
into pure value comparisons. So rather than calling:
x.equals(null)
it can do:
null.equals(x)
which is equivalent to:
null === x

The === and !== operators just compare the pure values provided, so for
objects, arrays, and functions the pointers will be compared, while for
tuples and compound types, the members will be compared recursively with
===.
For primitives, == and === both do the same thing, which is to compare
the actual values - not just the bit patterns, so:
(cast<byte> -1) ==  (cast<ubyte> 255)
(cast<long> -1) === (cast<ulong> 0xffffffffffffffff)
are both false.

This commit also adds extensive tests for the new equality operators and
methods, including one for an unintuitive edge case with generic arrays:

Since generic arrays do not store RTTI for the type parameters they
contain, and just replace it with references to the object type, it is
impossible to distinguish two arrays of the same generic type once they
have been converted to object. So, if Foo<uint> makes an empty []T where
T=uint, and stores it as an object, and then Foo<double> comes along and
compares an empty []T where T=double with it, then the arrays will
compare as equal despite their intended types not matching.