Skip to main content

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 calls kfree, which does nothing.
  • print - prints a short, one line representation of the object to the PrintBuffer
  • inspect - prints a multi-line description of the object to the PrintBuffer. 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 appropriate type, 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, the relocate 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 squares, 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:

  1. The implementation is sometimes wrong, likely confusing doing pointer math (steps by array stride) with address math (steps by one byte).
  2. 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, structures and basics 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.