Interesting TADS 3 Quirk [regarding list object properties]

Okay, so, this one is really weird. I’m running this on Gargoyle, in case it’s an implementation thing.

Disclaimer: This is not the exact code that I’m using in my project; this is just a simple, minimalist example for understanding. (I’m using character names from an audio book I’m re-listening to for this example.)

Anyways, let’s say I have the following Actor and FriendObject definitions:

siriKeaton: Actor { 'Siri Keaton'
    "Just your everyday synthesist, stuck with a vampire."

    friendsList = [
        new FriendObject('Isaac Spindel', 3),
        new FriendObject('Jukka Sarasti', 0)
    ]

    afterAction() {
        inherited();
        local jukka = friendsList[2];
        extraReport('\bJukka's friend level before: '
            + jukka.friendLevel);
        jukka.friendLevel++;
        extraReport('\bJukka's friend level after: '
            + jukka.friendLevel);
    }
}

class FriendObject: object {
    construct(_name, _friendLevel) {
        name = _name;
        friendLevel = _friendLevel;
    }

    // Default values
    name = 'Unnamed'
    friendLevel = 0
}

If I use the WAIT command a few times, I will get an output like:

> wait
Time passes.

Jukka's friend level before: 0
Jukka's friend level after: 1

> wait
Time passes.

Jukka's friend level before: 0
Jukka's friend level after: 1

This is not a typo! You are reading the output correctly!

For some weird reason, the friendLevel property of Siri’s friendsList[2] object seems to reset after every turn. I’m not sure why.

Now, if we instead define the list in the preinitThing() function, like so:

siriKeaton: Actor { 'Siri Keaton'
    "Just your everyday synthesist, stuck with a vampire."

    friendsList = []

    preinitThing() {
        inherited();
        // Append objects to list
        friendsList += new FriendObject('Isaac Spindel', 3);
        friendsList += new FriendObject('Jukka Sarasti', 0);
    }

    afterAction() {
        inherited();
        local jukka = friendsList[2];
        extraReport('\bJukka's friend level before: '
            + jukka.friendLevel);
        jukka.friendLevel++;
        extraReport('\bJukka's friend level after: '
            + jukka.friendLevel);
    }
}

Then we suddenly get the following output:

> wait
Time passes.

Jukka's friend level before: 0
Jukka's friend level after: 1

> wait
Time passes.

Jukka's friend level before: 1
Jukka's friend level after: 2

For some weird reason, if we define a list this way, the changes we made to the objects inside will carry over to the next turn!

Again, I’ve obviously found the workaround (after hours of manic searching and bugfixing), but I thought it would be useful for other TADS 3 users to know this is a problem. I’m using the adv3Lite library, but I’m pretty sure this has something to do with how TADS 3 handles object definitions, lists, and turn states.

So this is another post I’m making for the poor sod who is stuck with a hidden bug, and is desperately searching online for an answer.

Yeah, I’ve pounded my head against this previously, and I agree it’s very counterintuitive. By default TADS treats a property declaration like foo = [ 'bar'] to mean “each time obj.foo is evaluated, create a new list [ 'bar' ] and return it”. In order to get the behavior you (and I) expect, you need something like foo = static [ 'bar' ].

So in your example, you want something like

bob: Actor {
        friendsList = static [
                new FriendObject('Alice', 1);
        ]
}
1 Like

Also if friendsList is going to be changing much, it’s better to use static new Vector… List objects are always being copied/recreated, so it’s easy for unexpected subtleties to creep in. The differences between List and Vector are explained in the system manual and elsewhere…

1 Like

Ooooooo today is a GREAT day when I get to learn more cool things about my favorite IF authoring language!! Many thanks to @jbg and @johnnywz00!! I seem to have missed these nuances when reading the docs!

1 Like

Oh my god, I finally did some reading on vectors. I don’t know how I missed these this whole time. This would have saved one of my old projects, which took place in an environment run by cellular automata rules. In my naivety, I used lists to evaluate everything, and after every turn I had to wait 3 seconds for the prompt to come back, and I’m using a pretty beefy CPU and RAM combo.

I should have used vectors. Wow. All of my garbage collection tricks were doing absolutely nothing because lists apparently don’t use reference semantics, because vectors do that… The poor garbage collector was filling with so much data after every turn…

Also, @jbg, I’ve found the entry for static in the manual. While I understand what it does now, I don’t think my brain has caught up yet with what it implies about the handling of variables and object creation. Its use for lists and vectors is crystal-clear, though!

1 Like