Progress with Doomsday Script

Doomsday Script (DS) is the Doomsday 2 scripting language, built right into the core of the engine. Its syntax is heavily Python-inspired, however with a somewhat smaller set of language features. Recently I’ve been improving DS with future needs in mind.

Having a scripting language fully integrated with the rest of the engine is very powerful: subsystems can use the native DS classes (like de::Record) to store and manipulate data, making it instantly accessible from scripts as well.

While DS is available in Doomsday today, it is not yet taken advantage of everywhere internally. The most heavily used part is currently engine bootstrapping: persistent/default configuration and possible startup scripts (e.g., when upgrading to a new version that requires some changes).

After hearing about Swift, I was inspired to do some enhancements to Doomsday Script. The DS language has always lacked any object-oriented features, which is a huge omission considering that Doomsday 2 is fully object-oriented itself. I’ve spent the last few days adding the following new language features:

  • Any record can be considered a class from which other records can be instantiated. In practice this is done using the call operator: calling a record will produce a new instance. This means that any object can act as the class for another object.
  • If a record has a member called __super__, it is expected to contain an array with all the superclasses — multiple inheritance is possible. Symbol lookup automatically now extends to cover the members of superclasses after the record itself has been checked.
  • The member operator (.) can be used with any value: if the left-side operand points to a record, the right-side operand is looked up from that record; otherwise the value can specify a record from which members can be found. For instance, a DictionaryValue uses the built-in Core.Dictionary class that has native functions for operations like keys() and values(). This makes C-style functions like dictkeys(var) obsolete in favor of var.keys() kind of OO syntax.
  • A self variable is automatically set when calling functions in relation to an object. This means that, unlike Python, the methods of a class do not need to have self spelled out as one of the arguments. One still has to use self inside member functions, though, to refer to the object being acted on.
  • Symbol lookup was augmented with a special syntax for specifying where to look up the symbol. Since self is not an argument, this is needed for accessing shadowed members from superclasses.

The syntax for defining a new class is simple:

record MyClass()
    def __init__()
        print 'Initializing MyClass'
    end
end

As in Python, the __init__ method is automatically called when the class is instantiated:

obj = MyClass()    

A subclass needs the special -> notation to access shadowed members of the superclass:

record SubClass(MyClass)
    def __init__()
        self.MyClass->__init__()
    end
end

The part with MyClass-> tells the DS interpreter to look up __init__ from MyClass instead of self. However, when the function is called, self is still passed as the invocation scope, since it’s on the left side of the member operator.

I decided to go with -> instead of the C++ style :: because the latter already occurs in the slice operator, and the lexical analyzer does not know if :: here refers to the scope keyword or slice operator:

rev = array[::-1] # Reversed array.

These new object-oriented features will be crucial in the future. For instance, every File object in DS can now use the same superclass that contains the functions for manipulating a file object.

The plan is to have DS form the foundation of many of the engine’s subsystems: the “Quake-style” interactive console, InFine and game menus, XG (including new map scripting extensions), runtime help database, and possibly even game logic extensions like defining new types of objects via scripts. In most of these cases, object-orientation will prove very valuable.