(TADS3) I don't understand why inner classes are doing this.

I stripped down one of the root causes of why I’m having such a hard time with what I’m working on, and I have a simple example below of a little dummy TADS game file that gives unexpected behavior. What am I not understanding here?

The problem is with inner classes. If I define a class like this:

class MyClass : Thing
  // stuff
  innerObj : Object
    // stuff
  ;
;

Then I would expect that each time I make an instance of MyClass, it would make an instance of innerObj inside it also. But it doesn’t. I made a proof that it doesn’t by using PreinitObject objects that would print messages reporting their existence when first running. Here is the code:

#include <adv3.h>
#include <en_us.h>


gameMain: GameMainDef
  initialPlayerChar = me

  showIntro() { "A small game file to test how TADS behaves. "; }

;

versionInfo: GameID
  name = 'dummy for this test'
  byline = 'dummy for this test'
  authorEmail = 'dummy for this test'
  desc = 'So far this is a dummy placeholder string'
  version = '1' // dummy for this test
  IFID = 'ffffffff-ffff-ffff-ffff-aaaaaaaaaaaa' // dummy for this test
;

class MyType : Thing, PreinitObject
  name = 'dummy MyType'
  execute() 
  {
    // This is a debug test so at execution time it will print
    // a report from each instance of this class that exists:
    "Preinit Debug: I am an instance of MyType with a name of '<<name>>'.\n";
  }

  innerObj : Thing, PreinitObject
  {
    location = lexicalParent
    name = 'inside dummy'
    execute() 
    {
      // This is a debug test so at execution time it will print
      // a report from each instance of this class that exists:
      "Preinit Debug: I am an instance of innerObj inside of an object with a name of '<<location.name>>'.\n";
    }
  }
;


testingRoom: Room 
  'Testing Room'
  "This is the testing room. \n"
;
+ me: Actor
  name = 'player'
  npcDesc = "your self"
; 
// Two objects of type "MyType" with different names so when their
// PreinitObject execute() methods run, they will report different
// names and I can tell them apart.
+ outerInstance1 : MyType 'red thing' 'red thing' "A red thing"
;
+ outerInstance2 : MyType 'blue thing' 'blue thing' "A blue thing"
;

This is the output I get when I run this:

$ frob -i plain tadsexperiment.t3
Preinit Debug: I am an instance of MyType with a name of 'red thing'.
Preinit Debug: I am an instance of MyType with a name of 'blue thing'.
Preinit Debug: I am an instance of innerObj inside of an object with a name of 'dummy MyType'.
A small game file to test how TADS behaves.
Testing Room
This is the testing room.

You see a red thing (which contains an inside dummy) and a blue thing (which
contains an inside dummy) here.  In the dummy MyType, you see an inside dummy.


>quit
Do you really want to quit? (Y is affirmative) > y
[Hit any key to exit.]   

Notice how despite the fact that two messages print for the outer things, one for ‘red thing’ and one for ‘blue thing’, only one message prints for the innerObj, and furthermore, it’s for neither of the two actual instances of the outer object. It’s contained inside of the class (with it’s dummy name). Also, notice the odd printout of the room contents. It knows the ‘red thing’ contains an inner object and that the ‘blue thing’ also contains an inner object, and yet these other two inner objects never appear in the Preinit output. I suspect the engine only made one instance of the inner object, and added a reference to that one instance to the contents of all the outer things (which I’m sure would get buggy if I tried running this game further past this point.)

This is at the core of the problem I’m having with using Actor States in an inheritable way. I can’t seem to find a way to say “all instances of objects of type Foo will also contain instances of objects of type Bar which contain location properties that point to the parent Foo they are in”. I have to do this in order to make an Actor subclass that always contains an instance of a certain ActorState that always contains an instance of a certain ConvNode. The linkage between these object’s location properties doesn’t seem to be inheritable.

I know the usual reasons for saying it’s bad to manipulate location directly and moveInto() should always be used instead. Keep in mind, though, that moveInto() is code, and I’m trying to make a solution that has all the data values set correctly in the compiled image BEFORE any code executes. This is necessary because I don’t get to control when the adv3 library runs its own Preinit code (It runs before my Preinits run - I checked), and its that Preinit code that sets things up in the ActorStates and ConvNodes. I need the static image the compiler generates to already have the variables set up correctly before I get to run anything executable like moveInto().

An inner object only places that object inside the scope of the enclosing class/object. It’s an actual instance, not just a declaration. What you want instead is dynamic object creation:

class InnerObj: Thing {
    // stuff
}

class MyClass: Thing {
    myInnerObj = nil;

    initializeThing()
    {
        inherited();
        myInnerObj = new InnerObj;
    }
}

initializeThing() gets called on all Thing instances at preinit time.

Do I have a guarantee that when the tads game engine runs the executable image, all the initialize() methods will always run before all the execute() methods of PreinitObjects? If so this could be a solution. (The proper data relationships between ConvNodes, ActorStates, and Actors have to exist before the adb3 library’s PreinitObjects run.)

Thanks for the advice. It doesn’t exactly work in my case because I need this for ActorState and ConvNode objects, and those classes are not derived from class Thing and therefore don’t have initializeThing methods. But it still gave me the idea to do the same type of thing inside the construct() method instead, and that does work with any sort of Object in Tads as all Objects obey the construct() paradigm. The only downside is that this means I must make everything dynamic with the “new” operator since consctruct() does not run on objects instanced directly in the source (for some reason).

Okay I utterly give up on trying to make it possible to implement this via subclassing. Now the latest problem I’m running into is that there’s no logical ordering to when construct() gets called as opposed to calling Preinit’s execute(). Some properties that are constructed like so:

prop = new something()

get their constructors run before execution. But other things don’t. It seems to be dependent on the when the first use of the property is. I.E.

class OuterThing: Thing
  name = 'OuterName'
  prop = new InnerThing(); // This is where I would expect the constructor for InnerThing to happen.

  construct() 
  {
     "(debug: I'm an OuterThing being constructed.  My prop has a name of ";
      "'<<prop.name>>'.";  // But this is where the constructor to InnerThing is actually happening.
      ") ";

  } 
;
class InnerThing: Thing
  name = 'InnerName'
  construct()
  {
    "(debug: I'm an InnerThing being constructed.)\n";
  }
;
myInstance : MyClass
;

Guess what order the messages print out in when compiling this?
Does InnerThing get constructed first then OuterThing? Does OuterThing get constructed and then this causes it to construct an InnerThing when it hits the ‘prop =’ line? Neither. The InnerThing prop gets constructed late - not until the very first time something tries to make use of it - the <<prop.name>> part.

It gives this output:

(debug: I’m an OuterThing being constructed. My prop has a name of ‘(debug: I’m an InnerThing being constructed.)InnerName’.

Until I tried to access the prop’s “name” property, prop didn’t even exist at all yet. It got created on the fly as needed.

This is a big problem if I’m trying to make sure all the data exists before the Preinits’ execute()'s run.

Not tested, but if you change

prop = new something();

to

prop = static new something();

it should work. The “static” declaration tells the compiler to run the code at compile-time. (The System Manual’s article on object definitions has more info on the “static” keyword.)

You’re correct, of course. But that doesn’t solve the ultimate problem which is that I need to get the location properties set correctly before all the PreinitObjects’ execute()s are called. Why doesn’t it solve it? Because it turns out the order the compiler chooses to resolve those static references in isn’t under my own control, and it’s not in a useful order. Consider the following example file I just compiled and ran:

#include <adv3.h>
#include <en_us.h>


gameMain: GameMainDef
  initialPlayerChar = me

  showIntro() { "A small game file to test how TADS behaves. "; }

;

versionInfo: GameID
  name = 'dummy for this test'
  byline = 'dummy for this test'
  authorEmail = 'dummy for this test'
  desc = 'So far this is a dummy placeholder string'
  version = '1' // dummy for this test
  IFID = 'ffffffff-ffff-ffff-ffff-aaaaaaaaaaaa' // dummy for this test
;

class MyConvNode : ConvNode
  // other stuff not shown
  name = 'myconvnode'
  construct(loc)
  {
    inherited();
    location = loc;
    // Must use tadSay() because the output formatter isn't ready yet at
    // the point when this constructor runs:
    tadsSay( 'debug: constructing a MyConvNode called <<name>>...\n');
    tadsSay( '    ... and it\'s location is in a <<location?location.name:'nil'>> ...\n');
    tadsSay( '    ... and it\'s location\'s location is <<location.location?location.location.name:'nil'>> .\n' );
  }
;
class MyActorState : ActorState
  // other stuff not shown
  name = 'myactorstate'
  construct(loc)
  {
    inherited(loc);
    location = loc;
    tadsSay('debug: constructing a MyActorState in location <<location?location.name:'nil'>>.\n' );
  }
  cNode = static new MyConvNode(self)
;



startRoom: Room 
;
// Then elsewhere you make an instance of it like so
// to try to make Janet be an actor who has this actor state:
+janet : Actor 'Janet' 'Janet' "A woman named Janet."
  janetState = static new MyActorState(self)
;
+me: Actor 'Player' 'player' "You, the player"
;

The output it produces is this:

The output line shown in purple is the reason this code makes the library crash. It put the instance of the MyConvNode object inside the MyActorState object BEFORE it put the MyActorState object inside the janet object, so at the time it built the ConvNode object, the location of the containing actor wasn’t initialized yet.

I realize this bit looks weird:

+janet : Actor 'Janet' 'Janet' "A woman named Janet."
  janetState = static new MyActorState(self)
;

and it seems like it should have been written as:

+janet : Actor 'Janet' 'Janet' "A woman named Janet."
;
++  janetState : MyActorState
;

But I’ve actually tried it both ways and the output is the same. The “++” syntax was my first attempt and I thought that making all the constructor calls explicit might help but it didn’t.

I don’t think you can do what you want at compile time. Static initializers are a bit of a trap here since you will end up with a single copy of the instantiated object in the parent class, and anything that inherits from the class will share the same object unless it overrides the property holding the object reference.

You can get around that by creating a new object in the class constructor, but then you don’t need the static initializer at all. (You could also use the perInstance macro.)

class MyConvActor : Actor
  initializeActor()
  {
    tadsSay( 'debug: running initializeActor during PreInit for ' + name + '...\n' );
    setCurState(new MyActorState(self));
    inherited();
  }
;

class MyActorState : ActorState
  // other stuff not shown
  name = 'myactorstate'
  construct(actor)
  {
    inherited(actor);
    tadsSay('debug: constructing a MyActorState in location <<location?location.name:'nil'>>.\n' );
    local cNode = new MyConvNode(self);
    actor.convNodeTab[cNode.name] = cNode;
  }
;

class MyConvNode : ConvNode
  // other stuff not shown
  name = 'myconvnode'
  construct(loc)
  {
    inherited();
    location = loc;
    // Must use tadSay() because the output formatter isn't ready yet at
    // the point when this constructor runs:
    tadsSay( 'debug: constructing a MyConvNode called <<name>>...\n');
    tadsSay( '    ... and it\'s location is in a <<location?location.name:'nil'>> ...\n');
    tadsSay( '    ... and it\'s location\'s location is <<location.location?location.location.name:'nil'>> .\n' );
  }
;

bob : MyConvActor 'bob' 'bob' @lab
	"It's bob!"
;

Ah, then that is where I was going wrong. This is very non-intuitive. I would have assumed that if a property in a class definition is given as an expression rather than a fixed value, that this means the expression is evaluated in each instance of that class as the instance is being made. If what you’re saying is true, then that would mean the compiler has to secretly create a sort of master instance of that class in its head, and then for each instance of the class in the source code it just makes a raw copy of that master instance rather than re-running the expressions in the class’s property assignment code.

Otherwise I’d expect the new operator to be run for each instance of a class. This is what I was trying to do before I added the static keyword, but that didn’t work either, with or without the static keyword.

Right, I agree that it’s non-intuitive if you approach it from the perspective that a class is a definition. Inheritance in TADS 3 is prototype-based (like JavaScript) rather than class-based (like Java).

Put another way, a class is just another object; the additional semantics the class keyword provides are mostly related to information hiding. Class objects aren’t in scope for player input, and are excluded by default when you iterate through all the objects in the game using firstObj() / nextObj().

Objects are cheap; they’re not full clones of their parent object, they’re just the delta. A given object is identical in all respects to the object it inherits from, unless it directly overrides a property (or method.)

On a related note, all objects share the same property namespace. The property value will be nil if the object doesn’t define it, or inherit from something that does, but it’s there and it’s always legal to access it.

Have you considered writing an initialize() method that you then execute during preinit using the same mechanism as initializeThing()?

(See lib/adv3/misc.t, adv3LibPreinit object, around line 697. This is where Things, SpecialTopics, etc, are initialized.)

That also explains the problem I was having with doing it without the static keyword. The properties must have been waiting until the first time they were being used in some expression (and therefore caused a delta to happen) before their initialization code executed, which was hard to force to happen before Preinit.

It would be nice if there was a generic initialize() method that always worked on all objects (as opposed to an initializeThing that only works on Things, and an initializeActor that only works on Actors, etc.)

I think you need to use something like “execBeforeMe”. You use these to ensure the order is correct (to avoid crashes like the one you describe above)

Below is some copyright abuse from the site: tads.org/t3doc/doc/sysman/init.htm

Run-time initialization

The library also defines a class called InitObject, which works the same way as PreinitObject, but is used during normal program start-up rather than pre-initialization. Just before the standard start-up code calls your main() routine, it invokes the execute() method on each instance of InitObject.

As with PreinitObject’s, you can use the execBeforeMe and execAfterMe properties in your InitObject instances to control the order of initialization.

You can roll your own universal initializeObject method:

PreinitObject
  execute() {
      forEachInstance(Object, function(obj) {
          if (obj.propType(&initializeObject) == TypeCode)
            obj.initializeObject();
      });
  }

  execAfterMe() {
      local lst = new Vector(50);
      forEachInstance(PreinitObject, function(obj) {
          if (obj != self)
              lst.append(obj);
      });
      return lst.toList();
  }

  initializeObject = nil
;

// test case
TopicEntry
  initializeObject() {
      tadsSay('debug: my TopicEntry init runs first during PreInit\n');
  }
;

I expect it’s not there by default because it’s so much more useful to initialize objects that participate in the world model (like Thing and its descendants).

I missed the mention of execAfterMe in the documentation on this page: tads.org/t3doc/doc/sysman/objdef.htm
I only knew about the existence of execBeforeMe, which is why I wasn’t able to figure out how to make conversationManager’s execute happen after my own objects’ execute()'s.

Hello! I know this is an ancient thread, but it’s one of the first that came up on Google when searching for classes with nested objects. I’ve been able to get it working and wanted to share (this is using the adv3lite library, but it could be easily adapted to adv3):

class Ship : Enterable {
    exteriorDoor = nil;
    bridge = nil;
    interiorDoor = nil;
    bridgeName = nil;
    bridgeDesc = nil;
    exDoorName = nil;
    exDoorDesc = nil;
    inDoorName = nil;
    inDoorDesc = nil;
    firstRoom = bridge;
   
    preinitThing()
    {
        addVocab(';;ship');
        exteriorDoor = new Door();
        exteriorDoor.name = exDoorName != nil ? exDoorName : 'exterior door';
        exteriorDoor.desc = exDoorDesc != nil ? exDoorDesc : 'This is the exterior door';
        exteriorDoor.moveInto(self);
       
        bridge = new Room();
        bridge.name = bridgeName != nil ? bridgeName : 'Ship\'s Bridge';
        bridge.desc = bridgeDesc != nil ? bridgeDesc : 'This is the ship\'s bridge';
       
        interiorDoor = new Door();
        interiorDoor.name = inDoorName != nil ? inDoorName : 'interior door';
        interiorDoor.desc = inDoorDesc != nil ? inDoorDesc : 'This is the interior door';
        interiorDoor.moveInto(firstRoom);
       
        exteriorDoor.otherSide = interiorDoor;
        interiorDoor.otherSide = exteriorDoor;
       
        bridge.out = interiorDoor;
       
        connector = exteriorDoor;
        remapIn = exteriorDoor;
        isListed = true;
       
        inherited();
    }
}