Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add a little 6model documentation.
  • Loading branch information
jnthn committed May 30, 2011
1 parent 1c65083 commit e08c5ca
Show file tree
Hide file tree
Showing 2 changed files with 258 additions and 0 deletions.
37 changes: 37 additions & 0 deletions docs/6model/faq.markdown
@@ -0,0 +1,37 @@
# FAQ
This document tries to collect various frequently asked questions
about 6model and provides answers to them.

## My language has objects but no methods; is 6model a bad fit?
Not really. You are not forced to implement methods in the
meta-object to handle add_method or find_method, and of course your
meta-object would thus have no storage allocated for a method table.
The only wastage is that you have no usage for the method cache
and v-table slots in the s-table. That means that per type (not
per object instance) you'd have a couple of wasted slots, each a
pointer in size. Thus on a 32-bit machine, 100 classes/types later,
you've still yet to "waste" a kilobyte.

## What is a type object, and how should I use it?
When you pair a meta-class and a representation together, an s-table
is created along with a type object. The type-object becomes your
"handle" to that pairing, and can be used to create instances of
the object. In some languages (Perl 6), you would also install the
type object into the namespace (package or lexpad), as the user's
view of the type. However, that is not at all needed.

For a prototype-based language, you probably only ever make one
type object. You can just stash it somewhere and use it whenever
you need to create a new instance. If new instances are always just
clones anyway, then you needn't even keep hold of it, just install
the initial instance you create wherever it should go and then it
can be cloned from there on in.

For a class based language, you'll always need to have *some* way
of identifying a class to make an instance of. The type object is
your handle for creating an instance. You may make a lookup table
of the type objects and map instantiations of types through that
table. If you install a "class object" of some kind and it is what
manages the instantiation then you could have it holding the handle.
How you handle it is really very dependent on how your language
exposes object creation.
221 changes: 221 additions & 0 deletions docs/6model/overview.markdown
@@ -0,0 +1,221 @@
# Overview

## What is 6model?
6model is a framework for building implementations of object
oriented features. It does not contain an implementation of
classes, interfaces, roles, prototype objects and so forth.
Instead, it provides you with a set of building blocks that
enable you to build these kinds of features as your language
needs them. To make an analogy with another part of the
compiler toolkit, we don't provide a full grammar for your
language, but do provide a grammar engine, quote parser,
operator precedence parser and other helpful building blocks
to get you started.

## 6model Workflow
In general, when you want to implement the object system for
your language using 6model, you will go through the following
workflow.

1. Identify the kinds of object oriented types your language
has and that you will need to implement. For example, in
Perl 6 we have - amongst other things - classes, roles and
enumerations. In Java, you'd have classes and interfaces.
In JavaScript, you have prototype objects.

2. For each of them, pick a suitable in-memory representation.
6model provides some of these out of the box. In a language
where you know all of the attributes an object will have
"up-front", you can pick a representation that will just
allocate a single piece of memory for an object and map
each attribute to a slot. In a language where you can get
attributes coming to exist at any point and an object is
just like a hash table or dictionary, you can pick a
representation where your object works just like a hash.

3. Having picked a representation - which will control how
your objects are allocated and store/fetch attributes -
you now implement meta-objects for each kind of object
oriented type in your language. A meta-object is simply
an object that responds to different events that occur
in the life of a type. For example, at declaration time
we create a new type, add methods, add a parent type (e.g.
inheritance), add attributes and so forth. Later on we
may locate methods, do dynamic type checks (e.g. is-a).
Implementing a meta-object just involves writing a method
for to handle each of these "events".

4. Compile the language's type declarations to calls on the
meta-object.

Thus it's the meta-object that decides how most aspects of a
type work: whether it can accept having more than one parent
(if it does at all), how it dispatches methods (if it does at
all) and so forth. However, certain aspects are handled by the
representation: allocation of an object, storage of attributes
and boxing/unboxing. In some VMs the representation also needs
to interact with the GC.

Essentially, meta-object + representation = full implementation
of some kind of object oriented type (e.g. a class). Usually,
you will just pick an existing representation and implement the
meta-object yourself. Notice this means that you can factor
your implementation of OO features using OO principles.

## Representations
Representations are low-level things that are closely tied to
the underlying VM. They do expose a common API over all of them,
however. The following operations are available on representations.

* **type_object_for** takes a meta-object and creates a type
object that represents the association of the provided
meta-object and this representation. What you do with the type
object depends on your language. You could see it as an
"instantiation handle" - you use it in order to create an
instance of an object with the given meta-object/representation
pairing.

* **instance_of** does exactly what it says - creates a new
instance. You can supply it with the type object OR with any
existing instance, it doesn't matter. Note that this really does
just allocate space in memory for the object; it's not any kind of
"high level" instantiation where attributes get pre-populated with
empty containers.

* **defined** basically tells you if the thing you have is a type
object or an actual instance. For many languages you'll not need
this. It may come in useful in some languages to distinguish static
and instance method dispatch.

* **get_attribute** looks up an attribute. It expects to give back an
object of some kind. There are variants for native types:
**get_attribute_int**, **get_attribute_num** and **get_attribute_str**.
All of them expect the object to access the attribute of. You may also
pass one or all of an attribute name, a class handle (for when
attributes are not keyed just one name) and an index for when it's
possible to access an attribute by an offset rather than just by name.
What's needed varies by representation.

* **bind_attribute** and its native friends are basically what you'd
expect, and look largely like get_attribute apart from they take
something to store in the attribute rather than doing a lookup.

* **hint_for** takes an attribute name and optional a class handle
and will hand back - if available - an offset that can also be used
to look up the attribute. Interesting in gradual or static typing
scenarios.

* 6model representations may provide direct support for boxing and
unboxing native types. This is exposed through [set|get]_[int|num|str]
operations. Details vary by representation.

## Meta-objects
A meta-object is simply an object that says how another object works.
Critically, it's __just an object__. It may have attributes and it
will certainly have methods. Like any other object, it also has a
meta-object that says how it works. You may be tempted to start
categorising objects into "normal objects" versus "meta-objects",
but it's important to understand that 6model makes no distinction.
A meta-object isn't any kind of special object, it's just an object
that happens to be serving a particular role.

Meta-objects contain methods that relate to certain events in the
lifetime of a type. Let's imagine we have a really simple language
where we can declare some kind of type that has methods, make
instances of it and call the methods (granted, the instances are a
bit useless until we add attributes). We could write (in NQP) the
following.

class SimpleMetaObject {
has %!methods;
method new_type() {
my $meta-object := self.new();
return pir::repr_type_object_for__PPs(
$meta-object, 'HashAttrStore');
}
method add_method($type, $name, $code) {
%!methods{$name} := $code;
}
method find_method($type, $name) {
%!methods{$name}
}
}

Notice how this meta-object is just written as a normal class. Of
course, the class itself has a meta-object saying how it works - it's
meta-objects all the way down. The next thing to note is that we just
used a hash in order to implement our method store. Creating a new
type and adding a method are just methods that we include.

Here's how we can use the meta-object to define a new type, and add
a method.

# Create a new type.
my $type := SimpleMetaObject.new();

# Add a method.
$type.HOW.add_method($type, 'drink', -> $self {
say("mmmm...Starobrno!")
});

The .HOW macro in NQP maps to the PIR get_how opcode and it is used to
get hold of the meta-object. We just pass a simple lambda expression in
to add_method in order to specify what happens when the method is called.
Finally, we can make an instance and call the method.

# Make an instance.
my $obj := pir::repr_instance_of__PP($type);

# Call the method.
$obj.drink();

Notice that we didn't ever call our find_method method ourselves. In
fact, this is the only place so far where 6model has actually given us
any kind of restriction on how our meta-object should look; the names of
add_method and new_type are conventions. Under the hood, 6model has gone
and used the find_method method on the meta-object when we did a method
call on the object.

A question you may be left with is why the convention to take the type
object as the first parameter in all of the methods of the meta-object.
This isn't needed when doing something more "class based", but is
important in a more prototype-OO-ish setting.

## Performance
So far we have seen that one would tend to implement things like method
dispatch and type checking by writing methods in the meta-object. This
may feel a little heavyweight for an operation that is very common, and
indeed it is. 6model thus provides you with the possibility to publish
some "caches" that expose views of certain aspects of the meta-object in
a more low-level way to the VM. Thus it is able to perform some frequent
opertaions much more quickly. Of note, it is possible to publish:

* A name to method cache, which is useful for dispatching
methods by name. This is useful in most dynamic languages,
and means that method dispatch can just be looking in a
hash table. Generally this presents a flat view, with the
methods from any parent types included so there is no
walking required at runtime.

* A v-table, which you can also thing of as an index to
method cache. This is useful in static or gradually typed
languages, where a method call can be mapped to looking for
the method in a certain slot. Since this boils down to a
(machine-level) array lookup, it's fast.

* A type check cache. This is useful for doing fast type
checks. It contains references to other types that the
current one could be considered as "doing"/"being". Thus
it is useful for is-a and does-a operations. The current
6model implementation has it being scanned linearly. This
is a low-level loop over a low-level array, so it should be
pretty fast, however.

The word "cache" is chosen very deliberately. It's important
that the meta-object knows that it is responsible for the
updaing of any of the extra views of itself that it chooses
to publish. Put another way, in 6model the meta-object itself
is always the authoritative source.

0 comments on commit e08c5ca

Please sign in to comment.