Method System
OpenGOAL has a virtual method system. This means that child types can override parent methods. The first argument to a method is always the object the method is being called on, except for new
.
All types have methods. Objects have access to all of their parents methods, and may override parent methods. All types have these 9 methods:
new
- like a constructor, returns a new object. It's not used in all cases, and on all types, and needs more documentation on when specifically it is used.delete
- basically unused, but like a destructor. Often callskfree
, which does nothing.print
- prints a short, one line representation of the object to thePrintBuffer
inspect
- prints a multi-line description of the object to thePrintBuffer
. Usually auto-generated by the compiler and prints out the name and value of each field.length
- Returns a length if the type has something like a length (number of characters in string, etc). Otherwise returns 0. Usually returns the number of filled slots, instead of the total number of allocated slots, when there is possibly a difference.asize-of
- Gets the size in memory of the entire object. Usually this just looks this up from the appropriatetype
, unless it's dynamically sized.copy
- Create a copy of this object on the given heap. Not used very much?relocate
- Some GOAL objects will be moved in memory by the kernel as part of the compacting actor heap system. After being moved, therelocate
method will be called with the offset of the move, and the object should fix up any internal pointers which may point to the old location. It's also called on v2 objects loaded by the linker when they are first loaded into memory.memusage
- Not understood yet, but probably returns how much memory in bytes the object uses. Not supported by all objects.
Usually a method which overrides a parent method must have the same argument and return types. The only exception is new
methods, which can have different argument/return types from the parent. (Dee the later section on _type_
for another exception)
The compiler's implementation for calling a method is:
- Is the type a basic?
- If so, look up the type using runtime type information
- Get the method from the vtable
- Is the type not a basic?
- Get the method from the vtable of the compile-time type
- Note that this process isn't very efficient - instead of directly linking to the slot in the vtable (one deref) it first looks up the
type
by symbol, then the slot (two derefs). I have no idea why it's done this way.
In general, I suspect that the method system was modified after GOAL was first created. There is some evidence that types were once stored in the symbol table, but were removed because the symbol table became full. This could explain some of the weirdness around method calls/definition rules, and the disaster method-set!
function.
All type definitions should also define all the methods, in the order they appear in the vtable. I suspect GOAL had this as well because the method ordering otherwise seems random, and in some cases impossible to get right unless (at least) the number of methods was specified in the type declaration.
Special _type_
Type
The first argument of a method always contains the object that the method is being called on. It also must have the type _type_
, which will be substituted by the type system (at compile time) using the following rules:
- At method definition: replace with the type that the method is being defined for.
- At method call: replace with the compile-time type of the object the method is being called on.
The type system is flexible with allowing you to use _type_
in the method declaration in deftype
, but not using _type_
in the actual defmethod
.
A method can have other arguments or a return value that's of type _type_
. This special "type" will be replaced at compile time with the type which is defining or calling the method. No part of this exists at runtime. It may seem weird, but there are two uses for this.
The first is to allow children to specialize methods and have their own child type as an argument type. For example, say you have a method is-same-shape
, which compares two objects and sees if they are the same shape. Suppose you first defined this for type square
with
(defmethod square is-same-shape ((obj1 square) (obj2 square))
(= (-> obj1 side-length) (-> obj2 side-length))
)
Then, if you created a child class of square
called rectangle
(this is a terrible way to use inheritance, but it's just an example), and overrode the is-same-shape
method, you would have to have arguments that are square
s, which blocks you from accessing rectangle
-specific fields. The solution is to define the original method with type _type_
for the first two arguments. Then, the method defined for rectangle
also will have arguments of type _type_
, which will expand to rectangle
.
The second use is for a return value. For example, the print
and inspect
methods both return the object that is passed to them, which will always be the same type as the argument passed in. If print
was define as (function object object)
, then (print my-square)
would lose the information that the return object is a square
. If print
is a (function _type_ _type_)
, the type system will know that (print my-square)
will return a square
.
Details on the Order of Overrides
The order in which you defmethod
and deftype
matters.
When you deftype
, you copy all methods from the parent. When you defmethod
, you always set a method in that type. You may also override methods in a child if: the child hasn't modified that method already, and if you are in a certain mode. This is a somewhat slow process that involves iterating over the entire symbol table and every type in the runtime, so I believe it was disabled when loading level code, and you just had to make sure to deftype
and defmethod
in order.
Assume you have the type hierarchy where a
is the parent of b
, which is the parent of c
.
If you first define the three types using deftype
, then override a method from a
on c
, then override that same method on b
, then c
won't use the override from b
.
If you first define the three types using deftype
, then override a method on b
, it will sometimes do the override on c
. This depends on the value of the global variable *enable-method-set*
, and some other confusing options. It may also print a warning but still do the override in certain cases.
Built in Methods
All types have these 9 methods. They have reasonable defaults if you don't provide anything.
new
The new method is a very special method used to construct a new object, like a constructor. Note that some usages of the new
keyword do not end up calling the new method. See the new
section for more details. Unlike C++, fields of a type and elements in an array are not constructed either.
The first argument is an "allocation", indicating where the object should be constructed. It can be
- The symbol
'global
or'debug
, indicating the global or debug heaps - The symbols
'process-level-heap
or'loading-level
, indicating whatever heaps are stored in those symbols. 'process
, indicating the allocation should occur on the current process heap.'scratch
, for allocating on the scratchpad. This is unused.- Otherwise it's treated as a 16-byte aligned address and used for in place construction (it zeros the memory first)
The second argument is the "type to make". It might seem stupid at first, but it allows child classes to use the same new
method as the parent class.
The remaining arguments can be used for whatever you want.
When writing your own new
methods, you should ignore the allocation
argument and use the object-new
macro to actually do the allocation. This takes care of all the details for getting the memory (and setting up runtime type information if its a basic). See the section on object-new
for more details.
delete
This method isn't really used very much. Unlike a C++ destructor it's never called automatically. In some cases, it's repurposed as a "clean up" type function but it doesn't actually free any memory. It takes no arguments. The default implementations call kfree
on what the allocation, but there are two issues:
- The implementation is sometimes wrong, likely confusing doing pointer math (steps by array stride) with address math (steps by one byte).
- The
kfree
function does nothing.
The kheap
system doesn't really support freeing objects unless you free in the opposite order you allocate, so it makes sense that delete
doesn't really work.
print
This method should print out a short description of the object (with no newlines) and return the object. The printing should be done with (format #t ...)
(see the section on format
) for more information. If you call print
by itself, it'll make this description show up in the REPL. (Note that there is some magic involved to add a newline here... there's actually a function named print
that calls the print
method and adds a newline)
The default short description looks like this: #<test-type @ #x173e54>
for printing an object of type test-type
. Of course, you can override it with a better version. Built-in types like string, type, boxed integer, pair, have reasonable overrides.
This method is also used to print out the object with format
's ~A
format option.
inspect
This method should print out a detailed, multi-line description. By default, structure
s and basic
s will have an auto-generated method that prints out the name and value of all fields. For example:
gc > (inspect *kernel-context*)
[00164b44] kernel-context
prevent-from-run: 65
require-for-run: 0
allow-to-run: 0
next-pid: 2
fast-stack-top: 1879064576
current-process: #f
relocating-process: #f
relocating-min: 0
relocating-max: 0
relocating-offset: 0
low-memory-message: #t
In some cases this method is overridden to provide nicer formatting.
length
This method should return a "length". The default method for this just returns 0, but for things like strings or buffers, it could be used to return the number of characters or elements in use. It's usually used to refer to how many are used, rather than the capacity.
asize-of
This method should return the size of the object. Including the 4 bytes of type info for a basic
.
By default this grabs the value from the object's type
, which is only correct for non-dynamic types. For types like string
or other dynamic types, this method should be overridden. If you intend to store dynamically sized objects of a given type on a process heap, you must implement this method accurately.
copy
Creates a copy of the object. I don't think this used very much. Just does a memcpy
to duplicate by default.
relocate
The exact details are still unknown, but is used to update internal data structures after an object is moved in memory. This must be support for objects allocated in process heaps of processes allocated on the actor heap or debug actor heap.
It's also called on objects loaded from a GOAL data object file.
mem-usage
Not much is known yet, but used for computing memory usage statistics.