Skip to content
  • Yorick Peterse's avatar
    Turn Boolean back into a trait and rework AND/OR · 99a57b22
    Yorick Peterse authored
    A few months ago I turned Boolean from a trait into an object. Back then
    the trait setup for Boolean was rather horrible and I thought using a
    regular object would be better. Unfortunately the use of an object
    brought various issues with it. For example, every method defined on
    True or False would also have to be defined (as a dummy method) on
    Boolean itself. It was also impossible to support code such as this:
    
        def example(other: do -> Boolean) -> Boolean {
          if true: {
            True
          }, false: {
            other.call
          }
        }
    
    This wouldn't work because "Boolean" is not guaranteed to implement the
    same methods as "True".
    
    One solution to this problem would be to introduce union types and
    define "Boolean" as a union like so:
    
        type Boolean = True | False
    
    While tempting I feel that union types are the wrong approach. Every
    union type can be replaced by a trait and traits in general are much
    more pleasant to work with. For example, consider the following union
    type:
    
        type Number = Integer | Float
    
    Now let's say this type is defined somewhere in the standard library or
    some other piece of code we can't easily modify, and we use it in a
    whole bunch of places. If we at some point want to also support a
    Complex or Rational we'd have to either define our own type, or somehow
    ask the author of the "Number" type to extend it. Both cases are a pain.
    
    With traits on the other hand this would not be an issue as we can
    simply implement it where necessary and we're good to go.
    
    Because of the above issues we now define "Boolean" as a trait. To
    support this one can now define a trait and later redefine it, but
    _only_ if the trait is empty. This allows us to define "Boolean" in the
    "std::bootstrap" module and later refine it in "std::boolean". Without
    this we would not be able to bootstrap the runtime as various modules
    depend on "Boolean" being present from the very beginning.
    
    With these changes we also no longer need the GetBooleanPrototype
    instruction and thus it has been removed. We also removed && and || in
    favour of "and" and "or". Both these methods take a block that is only
    evaluated when necessary. This means that instead of this:
    
        foo && bar
    
    You would write:
    
        foo.and { bar }
    
    This has the added benefit of automatically grouping expressions, making
    it easier to chain message sends. For example, instead of this:
    
        (foo && bar).if_true {
          ...
        }
    
    You can now write this:
    
        foo.and { bar }.if_true {
          ...
        }
    
    To make all of this work I also had to make some changes to the type
    system. In particular we need to support downcasting of blocks in
    certain cases. Take the following piece of code for example:
    
        def or(other: do -> Boolean) -> Boolean {
          if true: {
            True
          }, false: {
            other.call
          }
        }
    
    Here the block passed to `true:` would be inferred as `do -> True` while
    the block passed to `false:` would be inferred as `do -> Boolean`. This
    would then produce a type erro because `do -> Boolean` is not compatible
    with `do -> True`.
    
    To support this we now check if in such cases we can downcast the
    expected type (`do -> True` in this example) to the given type.
    Currently we only downcast the return type of a block, but we may
    add support for other cases in the future.
    99a57b22