Skip to content
Snippets Groups Projects
Unverified Commit d87be76d authored by Yorick Peterse's avatar Yorick Peterse
Browse files

Clean up Object, Conditional, and Boolean

This makes it easier to implement Conditional and Equal, and makes
Object a little bit less of a mess. These changes uncovered two bugs:

1. The compiler did not support looking up default methods on a trait
   when they were defined on Object.

2. Map.iter was not implemented correctly. If the last bucket in a Map
   was Nil, Map.iter would produce a Nil value. This has been fixed by
   the Iterator not producing values at all in this case.

Originally I started working on these changes in hopes of getting rid of
Object entirely. This will not be possible, as it makes Inko a bit
annoying to work with. For example, Conditional and Equal would have to
be implemented manually for every object, leading to a lot of
boilerplate. Instead, we'll keep Object for the time being.
parent 7705d01a
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -127,6 +127,13 @@ module Inkoc
end
 
def implement_trait(trait)
# This is a hack due to trait lookups being messy and leading to stack
# overflows. In the new Inko compiler we'll have to come up with a sane
# way of looking up default methods from a parent object.
trait.attributes.each do |symbol|
define_attribute(symbol.name, symbol.type) if symbol.type.method?
end
implemented_traits[trait.unique_id] = trait
end
 
Loading
Loading
Loading
Loading
@@ -9,16 +9,15 @@
# few methods that _must_ exist before we can import any other methods.
import std::trait
 
# Next up we import std::object, which defines a few methods other modules
# depend on.
import std::object
# The std::boolean module must be imported next since many other modules depend
# on the methods it defines. This module also refines the "Boolean" trait
# defined earlier in std::bootstrap.
import std::boolean
 
# Next up we import std::object. We do this before all other modules because
# similar to std::boolean the other modules depend on various methods defined in
# std::object.
import std::object
# Now that most of the crucial bits and pieces are in place we can start
# importing other modules. At this point the order of the imports doesn't matter
# much any more.
Loading
Loading
Loading
Loading
@@ -2,11 +2,9 @@
#
# In Inko boolean true is the object `True` and boolean false is the object
# `False`. Both are regular objects that are instances of the `Boolean` object.
import std::conditional::Conditional
import std::conversion::ToString
import std::hash::(Hasher, Hash)
import std::operators::Equal
 
impl Boolean {
# Returns the `Boolean` that is the opposite of `self`.
Loading
Loading
@@ -37,34 +35,9 @@ impl Conditional for Boolean {
}
}
 
impl Equal for Boolean {
# Returns `True` if the given `Boolean` equals `self`.
#
# `True` is only equal to `True` itself, and `False` is only equal to `False`
# itself.
#
# # Examples
#
# Checking if two booleans are equal:
#
# True == True # => True
#
# Checking if two booleans are not equal:
#
# True == False # => False
def ==(other: Self) -> Boolean {
_INKOC.object_equals(self, other)
}
# Returns `True` if the given `Boolean` does not equal `self`.
def !=(other: Self) -> Boolean {
(self == other).not
}
}
impl ToString for Boolean {
def to_string -> String {
'Boolean'
_INKOC.get_attribute(self, '@_object_name') as String
}
}
 
Loading
Loading
@@ -96,18 +69,6 @@ impl Conditional for False {
}
}
 
impl ToString for True {
def to_string -> String {
'True'
}
}
impl ToString for False {
def to_string -> String {
'False'
}
}
impl False {
def not -> Boolean {
True
Loading
Loading
# Types for Abstract Syntax Trees produced by the parser.
import std::compiler::source_location::SourceLocation
import std::operators::Equal
 
# A trait for providing and adhering to common behaviour across AST nodes.
trait Node {
trait Node: Equal {
# Returns the location of the AST node.
def location -> SourceLocation
}
Loading
Loading
@@ -16,14 +16,6 @@
# object NullUser {}
#
# impl Conditional for NullUser {
# def if_true!(R)(block: do -> R) -> ?R {
# Nil
# }
#
# def if_false!(R)(block: do -> R) -> ?R {
# block.call
# }
#
# def if!(R)(true: do -> R, false: do -> R) -> R {
# false.call
# }
Loading
Loading
@@ -32,27 +24,35 @@
# let user = NullUser.new
#
# user.if(true: { 10 }, false: { 20 }) # => 20
#
# When implementing this trait, one should redefine `if` if they want to
# customise the behaviour. All other methods make use of this method, and will
# thus follow the new behaviour automatically.
trait Conditional {
# Calls the supplied block if the receiver evaluates to true.
# Calls the `true` block if the receiver evaluates to `True`, otherwise the
# `false` block is called.
#
# When the receiver evaluates to True the return value will be whatever the
# supplied block returned. When evaluating to False this method will instead
# return Nil.
def if_true!(R)(block: do -> R) -> ?R
# The return value is whatever the block returned. Both the blocks must
# return a value of the same type.
def if!(R)(true: do -> R, false: do -> R) -> R
 
# Calls the supplied block if the receiver evaluates to false.
# Calls the supplied block if the receiver evaluates to `True`.
#
# When the receiver evaluates to False the return value will be whatever the
# supplied block returned. When evaluating to True this method will instead
# When the receiver evaluates to `True` the return value will be whatever the
# supplied block returned. When evaluating to `False` this method will instead
# return Nil.
def if_false!(R)(block: do -> R) -> ?R
def if_true!(R)(block: do -> R) -> ?R {
if(true: block, false: { Nil })
}
 
# Calls the `true` block if the receiver evaluates to True, otherwise the
# `false` block is called.
# Calls the supplied block if the receiver evaluates to `False`.
#
# The return value is whatever the block returned. Both the blocks must
# return a value of the same type.
def if!(R)(true: do -> R, false: do -> R) -> R
# When the receiver evaluates to `False` the return value will be whatever the
# supplied block returned. When evaluating to `True` this method will instead
# return Nil.
def if_false!(R)(block: do -> R) -> ?R {
if(true: { Nil }, false: block)
}
 
# Returns `True` if `self` and the given object evaluate to `True`.
#
Loading
Loading
@@ -65,7 +65,9 @@ trait Conditional {
# Using and object and `False`:
#
# Object.new.and { False } # => False
def and(other: do -> Boolean) -> Boolean
def and(other: do -> Boolean) -> Boolean {
if(true: { other.call }, false: { False })
}
 
# Returns `True` if `self` or the given object evaluates to `True`.
#
Loading
Loading
@@ -78,5 +80,7 @@ trait Conditional {
# Using an object and `False`:
#
# Object.new.or { False } # => True
def or(other: do -> Boolean) -> Boolean
def or(other: do -> Boolean) -> Boolean {
if(true: { True }, false: { other.call })
}
}
Loading
Loading
@@ -287,21 +287,26 @@ object Map!(K: Hash + Equal, V) {
# }
def iter -> Iterator!(Pair!(K, V)) {
let mut index = 0
let mut found: ?Pair!(K, V) = Nil
let max = @buckets.length
 
Enumerator.new(
while: { index < max },
yield: {
let mut found: ?Pair!(K, V) = Nil
while: {
{
(index < max).and { found == Nil }
(index < max).and { found.nil? }
}.while_true {
found = @buckets[index]
index += 1
}
 
found
found != Nil
},
yield: {
let pair = found!
found = Nil
pair
}
)
}
Loading
Loading
@@ -481,7 +486,7 @@ object Map!(K: Hash + Equal, V) {
#
# This early return ensures we don't iterate over all buckets if we are
# certain we won't be able to find the key.
(pair == Nil).if_true {
pair.nil?.if_true {
return
}
 
Loading
Loading
# Functionality available to all objects.
#
# All objects in Inko are an instance of `Object`, and thus all its methods are
# available to all objects.
import std::conditional::Conditional
import std::operators::Equal
 
impl Object {
# Returns `True` if `self` and the given object are identical.
#
# Two objects are considered identical if they reside at the exact same
# memory address. This is also known as referential equality.
#
# This method should not be redefined by other objects, as doing so can break
# various parts of the Inko runtime.
def equal?(other: Self) -> Boolean {
_INKOC.object_equals(self, other)
}
}
impl Conditional for Object {
def if_true!(R)(block: do -> R) -> ?R {
block.call
}
def if_false!(R)(block: do -> R) -> ?R {
Nil
}
def if!(R)(true: do -> R, false: do -> R) -> R {
true.call
}
}
 
def and(other: do -> Boolean) -> Boolean {
if(true: { other.call }, false: { False })
}
def or(other: do -> Boolean) -> Boolean {
if(true: { True }, false: { other.call })
impl Equal for Object {
def ==(other: Self) -> Boolean {
equal?(other)
}
}
 
impl Equal for Object {
# Returns `True` if `self` and the given object are identical.
#
# Two objects are considered identical if they reside at the exact same
# memory address.
impl Object {
# Returns `True` if `self` is `Nil`.`
#
# # Examples
#
# Checking two equal objects:
#
# let obj = Object.new
#
# obj == obj # => True
# Checking if an object is `Nil`:
#
# Checking two objects that are not equal to each other:
#
# let obj1 = Object.new
# let obj2 = Object.new
#
# obj1 == obj2 # => False
def ==(other: Self) -> Boolean {
equal?(other)
}
def !=(other: Self) -> Boolean {
(self == other).not
# 10.nil? # => False
# Nil.nil? # => True
def nil? -> Boolean {
_INKOC.object_equals(self, Nil)
}
}
Loading
Loading
@@ -49,22 +49,36 @@ trait Greater {
# The binary `==` operator.
trait Equal {
# Returns `True` if `self` and the given object are equal to each other.
#
# By default this method simply uses `Equal.equal?`, but objects can freely
# redefine `Equal.==` to change this behaviour.
def ==(other: Self) -> Boolean
 
# Returns `True` if `self` and the given object are not identical.
# Returns `True` if `self` and the given object are identical.
#
# Two objects are considered identical if they reside at the exact same
# memory address. This is also known as referential equality.
#
# This method should not be redefined by other objects, as doing so can break
# various parts of the Inko runtime.
def equal?(other: Self) -> Boolean {
_INKOC.object_equals(self, other)
}
# Returns `True` if `self` and the given object are not equal.
#
# # Examples
#
# Comparing two objects that are not identical:
#
# Object.new != Object.new # => True
# 10 != 20 # => True
#
# Comparing two objects that are identical:
#
# let obj = Object.new
#
# obj != obj # => False
def !=(other: Self) -> Boolean
# 10 != 10 # => False
def !=(other: Self) -> Boolean {
_INKOC.object_equals(self == other, False)
}
}
 
# The binary `>=` operator.
Loading
Loading
Loading
Loading
@@ -965,20 +965,20 @@ test.group('Parsing closures') do (g) {
 
assert.equal(node.body.children.length, 1)
assert.true(node.arguments.empty?)
assert.true(node.return_type == Nil)
assert.true(node.throw_type == Nil)
assert.true(node.return_type.nil?)
assert.true(node.throw_type.nil?)
}
 
g.test('Parsing a closure with dynamically typed arguments') {
let node = parse_as(input: 'do (a, b) {}', type: Closure)
 
assert.equal(node.arguments[0].name, 'a')
assert.true(node.arguments[0].value_type == Nil)
assert.true(node.arguments[0].default_value == Nil)
assert.true(node.arguments[0].value_type.nil?)
assert.true(node.arguments[0].default_value.nil?)
 
assert.equal(node.arguments[1].name, 'b')
assert.true(node.arguments[1].value_type == Nil)
assert.true(node.arguments[1].default_value == Nil)
assert.true(node.arguments[1].value_type.nil?)
assert.true(node.arguments[1].default_value.nil?)
}
 
g.test('Parsing a closure with statically typed arguments') {
Loading
Loading
@@ -986,11 +986,11 @@ test.group('Parsing closures') do (g) {
 
assert.equal(node.arguments[0].name, 'a')
assert.equal((node.arguments[0].value_type as Constant).name, 'A')
assert.true(node.arguments[0].default_value == Nil)
assert.true(node.arguments[0].default_value.nil?)
 
assert.equal(node.arguments[1].name, 'b')
assert.equal((node.arguments[1].value_type as Constant).name, 'B')
assert.true(node.arguments[1].default_value == Nil)
assert.true(node.arguments[1].default_value.nil?)
}
 
g.test('Parsing a closure with a constant as the return type') {
Loading
Loading
@@ -1040,20 +1040,20 @@ test.group('Parsing lambdas') do (g) {
 
assert.equal(node.body.children.length, 1)
assert.true(node.arguments.empty?)
assert.true(node.return_type == Nil)
assert.true(node.throw_type == Nil)
assert.true(node.return_type.nil?)
assert.true(node.throw_type.nil?)
}
 
g.test('Parsing a lambda with dynamically typed arguments') {
let node = parse_as(input: 'lambda (a, b) {}', type: Lambda)
 
assert.equal(node.arguments[0].name, 'a')
assert.true(node.arguments[0].value_type == Nil)
assert.true(node.arguments[0].default_value == Nil)
assert.true(node.arguments[0].value_type.nil?)
assert.true(node.arguments[0].default_value.nil?)
 
assert.equal(node.arguments[1].name, 'b')
assert.true(node.arguments[1].value_type == Nil)
assert.true(node.arguments[1].default_value == Nil)
assert.true(node.arguments[1].value_type.nil?)
assert.true(node.arguments[1].default_value.nil?)
}
 
g.test('Parsing a lambda with statically typed arguments') {
Loading
Loading
@@ -1061,11 +1061,11 @@ test.group('Parsing lambdas') do (g) {
 
assert.equal(node.arguments[0].name, 'a')
assert.equal((node.arguments[0].value_type as Constant).name, 'A')
assert.true(node.arguments[0].default_value == Nil)
assert.true(node.arguments[0].default_value.nil?)
 
assert.equal(node.arguments[1].name, 'b')
assert.equal((node.arguments[1].value_type as Constant).name, 'B')
assert.true(node.arguments[1].default_value == Nil)
assert.true(node.arguments[1].default_value.nil?)
}
 
g.test('Parsing a lambda with a constant as the return type') {
Loading
Loading
@@ -1215,7 +1215,7 @@ test.group('Parsing variable definitions') do (g) {
let node = parse_as(input: 'let number = 10', type: DefineVariable)
 
assert_instance_of(node.name, Identifier)
assert.true(node.value_type == Nil)
assert.true(node.value_type.nil?)
assert.false(node.mutable?)
 
assert.equal((node.name as Identifier).name, 'number')
Loading
Loading
@@ -1227,7 +1227,7 @@ test.group('Parsing variable definitions') do (g) {
let node = parse_as(input: 'let Number = 10', type: DefineVariable)
 
assert_instance_of(node.name, Constant)
assert.true(node.value_type == Nil)
assert.true(node.value_type.nil?)
assert.false(node.mutable?)
 
assert.equal((node.name as Constant).name, 'Number')
Loading
Loading
@@ -1239,7 +1239,7 @@ test.group('Parsing variable definitions') do (g) {
let node = parse_as(input: 'let mut number = 10', type: DefineVariable)
 
assert_instance_of(node.name, Identifier)
assert.true(node.value_type == Nil)
assert.true(node.value_type.nil?)
assert.true(node.mutable?)
 
assert.equal((node.name as Identifier).name, 'number')
Loading
Loading
@@ -1250,7 +1250,7 @@ test.group('Parsing variable definitions') do (g) {
let node = parse_as(input: 'let mut Number = 10', type: DefineVariable)
 
assert_instance_of(node.name, Constant)
assert.true(node.value_type == Nil)
assert.true(node.value_type.nil?)
assert.true(node.mutable?)
 
assert.equal((node.name as Constant).name, 'Number')
Loading
Loading
@@ -1289,8 +1289,8 @@ test.group('Parsing method definitions') do (g) {
assert.true(node.arguments.empty?)
assert.true(node.type_parameters.empty?)
 
assert.true(node.return_type == Nil)
assert.true(node.throw_type == Nil)
assert.true(node.return_type.nil?)
assert.true(node.throw_type.nil?)
}
 
g.test('Parsing a method with dynamically typed arguments') {
Loading
Loading
@@ -1299,10 +1299,10 @@ test.group('Parsing method definitions') do (g) {
assert.equal(node.arguments.length, 2)
 
assert.equal(node.arguments[0].name, 'a')
assert.true(node.arguments[0].value_type == Nil)
assert.true(node.arguments[0].value_type.nil?)
 
assert.equal(node.arguments[1].name, 'b')
assert.true(node.arguments[1].value_type == Nil)
assert.true(node.arguments[1].value_type.nil?)
}
 
g.test('Parsing a method with statically typed arguments') {
Loading
Loading
@@ -1432,7 +1432,7 @@ test.group('Parsing return expressions') do (g) {
g.test('Parsing a return without a value') {
let node = parse_as(input: 'return', type: Return)
 
assert.true(node.expression == Nil)
assert.true(node.expression.nil?)
}
 
g.test('Parsing a return with a value') {
Loading
Loading
@@ -1446,7 +1446,7 @@ test.group('Parsing return expressions') do (g) {
let nodes = parse("return\n10").children
 
assert.equal(nodes.length, 2)
assert.true((nodes[0] as Return).expression == Nil)
assert.true((nodes[0] as Return).expression.nil?)
}
 
g.test('Parsing a return followed by a closing curly brace') {
Loading
Loading
@@ -1459,21 +1459,21 @@ test.group('Parsing return expressions') do (g) {
g.test('Parsing a return followed by a closing parenthesis') {
let node = parse_as(input: '(return)', type: Return)
 
assert.true(node.expression == Nil)
assert.true(node.expression.nil?)
}
 
g.test('Parsing a return followed by a comment') {
let nodes = parse('return # foo').children
 
assert.equal(nodes.length, 1)
assert.true((nodes[0] as Return).expression == Nil)
assert.true((nodes[0] as Return).expression.nil?)
}
 
g.test('Parsing a return followed by a documentation comment') {
let nodes = parse('return # foo').children
 
assert.equal(nodes.length, 1)
assert.true((nodes[0] as Return).expression == Nil)
assert.true((nodes[0] as Return).expression.nil?)
}
 
g.test('Parsing a return expression inside an index access expression') {
Loading
Loading
@@ -1483,7 +1483,7 @@ test.group('Parsing return expressions') do (g) {
 
assert_instance_of(node.arguments[0]!, Return)
 
assert.true((node.arguments[0] as Return).expression == Nil)
assert.true((node.arguments[0] as Return).expression.nil?)
}
 
g.test('Parsing a return expression that returns self') {
Loading
Loading
Loading
Loading
@@ -390,6 +390,29 @@ test.group('std::map::Map.iter') do (g) {
assert.equal(pair.key, 'name')
assert.equal(pair.value, 'Alice')
}
g.test('Iterators will skip trailing empty buckets') {
let map = Map.new
let pair1 = Pair.new(key: 'a', value: 'value', hash: 0)
let pair2 = Pair.new(key: 'b', value: 'value', hash: 1)
map.rehash
map._insert_pair(pair: pair1)
map._insert_pair(pair: pair2)
map.rehash
# At this point the bucket layout is: [pair, pair, Nil, Nil]
let iter = map.iter
assert.true(iter.next?)
assert.equal(iter.next, pair1)
assert.true(iter.next?)
assert.equal(iter.next, pair2)
assert.false(iter.next?)
assert.equal(iter.next, Nil)
}
}
 
test.group('std::map::Map.keys') do (g) {
Loading
Loading
Loading
Loading
@@ -86,3 +86,11 @@ test.group('std::object::Object.or') do (g) {
assert.true(obj.or({ False }))
}
}
test.group('std::object::Object.nil?') do (g) {
g.test('Checking if an object is Nil') {
assert.true(Nil.nil?)
assert.false(10.nil?)
assert.false(Object.new.nil?)
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment