Solveig

One hierarchy: everything is an object

Why a method added to object is answered by a number, where a value is still not an object, and the one place that difference shows. Every snippet here has been run; the outputs are what the VM actually prints.

For the design history — why this was long believed to need the class-side / instance-side split, and did not — see class-and-instance.md. For the messages themselves see REFERENCE.md.


The chain ends at object

Every built-in class delegates to object. It is ten lines in builtins.c:

vm->integer_class->proto = vm->object_class;
vm->float_class->proto   = vm->object_class;
vm->nil_class->proto     = vm->object_class;
vm->bool_class->proto    = vm->object_class;
vm->block_class->proto   = vm->object_class;
vm->array_class->proto   = vm->object_class;
vm->dict_class->proto    = vm->object_class;
vm->time_class->proto    = vm->object_class;
vm->string_class->proto  = vm->object_class;
vm->symbol_class->proto  = vm->object_class;

error gets it at birth, being made with sol_object_new(vm, vm->object_class). object itself has no prototype: the chain has to end somewhere, and this is where.

#45:isKindOf(object):print.            ; true
"s":isKindOf(object):print.            ; true
integer:parent:equals(object):print.   ; true
object:parent:print.                   ; nil   -- the chain ends here

So “everything is an object” is true of the type graph and not only of the slogan.

One definition, every receiver

That is what the root is for. A method bound on object is found from anything:

object:describe := { "understood by ":concat(self:asString) }.
#45:describe:display.               ; understood by 45
1.5:describe:display.               ; understood by 1.5
"s":describe:display.               ; understood by s
'sym:describe:display.              ; understood by sym
true:describe:display.              ; understood by true
nil:describe:display.               ; understood by nil
[#1, #2]:describe:display.          ; understood by [#1, #2]
{ #1 }:describe:display.            ; understood by <block>
dictionary:new:describe:display.    ; understood by <dictionary>
system:time:describe:display.       ; -- understood by the moment it runs
error:new:describe:display.         ; -- understood by <object 0x102dfa500>, at whatever address

One definition, eleven kinds of receiver. Reflection walks the same chain, so it agrees:

object:describe := { "understood by ":concat(self:asString) }.

#45:respondsTo('describe):print.   ; true
#45:isKindOf(integer):print.       ; true
#45:isKindOf(object):print.        ; true
#45:isKindOf(string):print.        ; false

The nearest slot still wins, exactly as it does anywhere else in a delegation chain — the root is the end of the search, not a special case in it:

object:describe  := { "some value" }.
integer:describe := { "a number" }.

#45:describe:display.        ; a number
"s":describe:display.        ; some value

A value is a kind of object, and is not an object

This is the distinction to hold on to. Delegating to object gives a value the behaviour; it does not give it the storage.

#45:parent.
solvm: 'parent' expects an object, got integer

#45:via(object).
solvm: 'via' expects an object, got integer

#45:x := #1.
solvm: cannot bind 'x' on integer

#45 is an immediate — a tagged word, not a heap object with a slot table. There is nowhere to put a slot, so it cannot carry state of its own, cannot be re-parented, and cannot be subclassed: an unboxed number’s class is chosen by its type tag, and there is nowhere to record a different one.

  a value: #45, "s", true an object: object:new
answers methods bound on object yes yes
isKindOf(object) true true
slots of its own no yes
parent, via refused answered
what new on its class does refuses, and says what to write makes one

Why this was safe to do

The two capabilities a value cannot honour were already refused before the root existed, which is the whole reason ten lines were enough.

via and parent are the only two messages integer does not define for itself, and both are installed as instance(vm->object_class, SOL_OBJ, ...) — the receiver requirement refuses anything that is not an object. That check went in with 1.6, three commits before anyone thought about a root.

The other worry was new. A built-in inheriting object:new would answer a plain object delegating to the class — for string, a thing that refuses every message a string understands. integer, float and array already shadowed it; the four left now refuse and teach:

integer:new(#45).
solvm: an integer is written #45, and there is nothing for 'new' to make -- #0 is the empty one

symbol:new.
solvm: a symbol is written 'name, or made from a string with asSymbol -- not with 'new'

block:new.
solvm: a block is written { ... } and compiled -- there is nothing for 'new' to make

boolean:new.
solvm: there are only two booleans, true and false -- 'new' makes neither

The rule underneath: new means “make an object delegating to me”, and those classes have instances that are not objects delegating to them.

The one place the difference shows

Override on a value class a method that object defines, and the override cannot call the one it displaced — because reaching past a nearer slot is via’s job, and via wants an object:

object:describe  := { "some value" }.
integer:describe := { "a number, and then ":concat(self:via(object):describe) }.

#45:describe.
solvm: 'via' expects an object, got integer

boundTo is the way through. It supplies a receiver to an already-fetched method rather than searching from one, so it takes a value happily:

object:slotAt('describe):boundTo(#45):value:display.    ; some value

That asymmetry is a leftover rather than a design: via was written to refuse non-objects back when a value’s chain ended at its own class, and now that every class delegates to object a value has a well-defined chain to walk. Recorded as a loose end in 2.14.

What it quietly removed

ideas.md used to carry this, to put one caseOf on numbers as well as objects:

integer:caseOf := object:slotAt('caseOf).

It has not been needed since the root: a method on object is found from a number like anything else. The single root took a paragraph of cleverness and made it unnecessary, which is the better outcome.

What it costs

The miss path only, and only because a failed lookup now walks object’s slots before giving up: 200,000 failed lookups cost about 10% more. A send that hits is unchanged, and the path that got slower is the one that ends in does not understand.