Jump to content
  • entries
  • comments
  • views

Om Prototype Inheritance and Object Destructors

Sign in to follow this  


Om Prototype Inheritance and Destructors

Been a while since an update since I've been mainly reimplementing all of the basic functionality into the now third incarnation of Om that has been already described in previous entries. I've made everything a lot more efficient than it was before and tried to keep function calls to a minimum inside the main run-time instruction despatch loop. Premature optimisation is a bit more acceptable in a general purpose scripting language I think.

I've recently sort of finished two main new areas. Prototype-based inheritance and object destructors. Will describe the prototype stuff first.

Om Prototype Inheritance

Prototype inheritance is borrowed from Javascript and seems to be the only obvious way to implement inheritance in a strictly dynamic and duck-typed language. Each object now has a special property - [font='courier new']prototype[/font] - which is handled differently to other object properties. It has to be assigned either [font='courier new']Om::Type::Null[/font] or [font='courier new']Om::Type::Object[/font]. It can be assigned by any of the normal ways you assign object properties but is handled differently under the hood. Here's an example of a program using the prototype-based inheritance.

var animal = func(n){ return { name = n; speak = func { out this.name, " says ", this.talk; }; };};var cat = func(n){ return { prototype = animal(n); talk = "meow"; };};var dog = func(n){ return { prototype = animal(n); talk = "woof"; };};var animals = [ dog("Erol"), cat("Felix"), cat("Frank") ];for(var a: animals) a.speak();Basically, a normal function is used in place of a constructor, that returns an object. The second two types assign the first [font='courier new']animal[/font] as their prototype, and when the runtime looks up a property of the object, it first checks the object's own properties, then searches up the prototype chain until it finds a match. This is only on read-only cases. When you assign to a member, it is always added to the main object's local ID set so in effect you inherit all of the defaults from the base object but can override these locally within a given object instance.

I might at some point add some syntatic sugar, a la Javascript's recent "class" keyword, but I like the flexibility and simplicity of using functions here, as you can even optionally assign different prototypes depending on the input to the function.

I found Javascript's whole use of the "new" keyword a bit messy and decided to keep it as simple as the above in Om. [font='courier new']prototype[/font] is just an object property like all others, it is just handled a bit differently under the hood and is stored separately from the object ID map so lookup is as fast as can possibly be.

The possibilities for this are endless and this simple example doesn't really do it justice, but I'm sure you can imagine the potential here. Being able to change the prototype during normal program flow is another interesting idea that I can't yet think of any examples of, but is quite possible under this system.

Reasing or writing to the [font='courier new']prototype[/font] using something like [font='courier new']o["prototype"][/font]is also quite possible but a runtime check to ensure you are assiging either [font='courier new']Om::Type::Null[/font] or [font='courier new']Om::Type::Object[/font] is always performed.

Setting a prototype increases the reference count of the prototype object and when the derived object is destroyed, it reduces its reference count, so you can use a temporary object as a prototype and its lifetime will persist until the last object referencing it goes out of scope.

Om Object Destructors

I appreciate that the use of destructors in a language that does not provide manual resource acquisition and release might not seem that useful, but I quite often find in C++ that I use RAII to represent things that might be useful in a scripting language. For example, setting a flag that needs to be unset when a function with multiple return points exits is a good example. Quick example:

var guard = func(var f){ f[0] = true; return { flags = f; destructor = func { f[0] = false; }; };};var f = func{ var flag; var g = guard([ flag ]); if(something) return; do_something();};That was just off the top of my head and might not even compile but you get the idea. I'm passing the flag into the guard object in an [font='courier new']Om::Type::Array[/font] so that it is by reference. When [font='courier new']g[/font] goes out of scope by any exit from [font='courier new']f()[/font], it should call the destructor which will clear the flag.

[font='courier new']destructor[/font] is like [font='courier new']prototype[/font], just a special object property that is stored separately and is called when the object in question's ref count reaches zero. A runtime check is always peformed to ensure that either an [font='courier new']Om::Value::Null[/font] or [font='courier new']Om::Value::Function[/font] is assigned to it.

In terms of inheritance, it doesn't work quite like we are used to in C++. If, for example, several derived objects share the same instance of an object as their prototype, the base object's destructor will only be called when the last object referencing it is destroyed, so you don't get quite the same chain of destruction as you get in C++.

This added quite a lot of unexpected complexity to the runtime since it is now possible for a decrement operation to itself cause an error if there is some duff code in the destructor so had to do a lot of rewriting to ensure that all dec operations can retrieve this error and pass it back to the user.

In some cases this is not possible. For example, if an object has its lifetime extended out of the script environnment by being assigned to an [font='courier new']Om::Value[/font] that persists in the native code, when the [font='courier new']Om::Value[/font] has its C++ destructor called that can cause the Om script destructor to be run, which could fire an error which has to be swallowed.

With this in mind, I've added a [font='courier new']Om::Value::destroy()[/font] method which releases whatever value is pointed to by the [font='courier new']Om::Value[/font] and changes the value to an [font='courier new']Om::Type::Null[/font], then returns an [font='courier new']Om::Value[/font] which could represent an error, for the cases where you might need to test for such a thing by manually destroying the [font='courier new']Om::Value[/font] rather than just letting it go out of its C++ scope.

Errors from destructors that occur during normal script operation are returned to the user like normal, but I have had to be careful in the case where stack unwinding is happening as a result of a higher level error so that the destructor error does not overwrite the original error that caused the call to [font='courier new']abort()[/font]. Think what I have now is correct but needs a lot of testing to be sure, as this took quite a lot of rewriting to get this working.

So we're at a good point now. All the basics are back in, in a fairly efficient manner and I can now get stuck in to new and more exciting features like the ones above. Will keep you all posted and thanks for reading.
Sign in to follow this  


Recommended Comments

There are no comments to display.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Advertisement

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!