Stupid LookupTable tricks: updating a nested value

Updating a LookupTable value appears to silently create a new LookupTable instance, instead of modifying the existing one. The documentation for LookupTable suggests that the class is based on Vector and this appears to contradict the documentation for Vector: the difference between Vector and List is that a Vector is modified in place and modifying a List leaves the original list intact and creates a new list.

Anyway, here’s an example to illustrate:

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>


startRoom:      Room 'Void'
        "This is a featureless void. "
;

me:     Person
        location = startRoom

        // Random nonsense in the form of a lookup table
        config = [
                'foozle' -> [
                        'value' -> 'default string',
                        'options' -> [ 'foo', 'bar' ]
                ]
        ]
        configFlag = nil

        // Given a list and an optional prompt, get input from the player
        // until they type something that matches one of the items from the
        // list.
        getWordFromList(lst, prompt?) {
                local i, n, txt;

                n = nil;

                // Set a default prompt if one wasn't given as an arg
                if(!prompt)
                        prompt = '\b&gt;';

                while(n == nil) {
                        // Display the prompt
                        "<<prompt>>";
                        // Get a line of input
                        txt = inputManager.getInputLine(nil, nil);
                        // Get the input, less any leading or trailing spaces
                        if(rexMatch('<space>*(<alpha>+)<space>*$', txt) != nil)
                                n = rexGroup(1)[3];
                        if(n) {
                                for(i = 1; i <= lst.length; i++) {
                                        // If the typed input is the starting
                                        // bit of one of the items in the list,
                                        // return the matching full item
                                        if(lst[i].startsWith(n))
                                                return(lst[i]);
                                }
                                // Nothing matched, prompt and try again
                                n = nil;
                        }
                }
                return(n);
        }
        // Handle a single config question.
        // The arg is the key for the stanza in the lookup table
        configQuestion(id) {
                local cfg, pr;

                // Get the requested stanza or give up
                cfg = config[id];
                if(!cfg) return;

                // Construct the question prompt from the options
                pr = cfg['options'].join('/');

                // Get the player input
                cfg['value'] = getWordFromList(cfg['options'], pr + ' >');

                // IMPORTANT:  This needs to be uncommented
                //config[id] = cfg;
        }
        desc() {
                local cfg;

                // First time through we prompt the player for input
                if(!configFlag) {
                        "Pick one:\n ";
                        configQuestion('foozle');
                        configFlag = true;
                }

                // We always output the configured value
                cfg = config['foozle'];
                "Foozle is <q><<cfg['value']>></q>. ";
        }
;

versionInfo:    GameID
        name = 'sample'
        byline = 'nobody'
        authorEmail = 'nobody <foo@bar.com>'
        desc = '[This space intentionally left blank]'
        version = '1.0'
        IFID = '12345'
;
gameMain:       GameMainDef
        initialPlayerChar = me
;

This game-like thing contains nothing but the starting room and the player. When the player does >X ME (or the equivalent) the first time, they’re prompted to make a choice: type FOO or BAR. This theoretically updates me.config['foozle']['value'] to be whatever the player entered, which is then displayed.

The code as presented above doesn’t work, however:

>x me
Pick one:
foo/bar >foo
Foozle is "default string".

Instead of updating the value, it stays the default. On the other hand if you edit the line marked IMPORTANT in the comments, so configQuestion() is now:

        configQuestion(id) {
                local cfg, pr;

                // Get the requested stanza or give up
                cfg = config[id];
                if(!cfg) return;

                // Construct the question prompt from the options
                pr = cfg['options'].join('/');

                // Get the player input
                cfg['value'] = getWordFromList(cfg['options'], pr + ' >');

                // IMPORTANT:  This needs to be uncommented
                config[id] = cfg;
        }

It now works:

>x me
Pick one:
foo/bar >foo
Foozle is "foo".

The only difference is that in the first example we set cfg to be config[id] and then cfg['value'] to be the player input, and stop there. In the working example, we then also set config[id] to be cfg before returning. Which will only matter if cfg was copied by value instead of reference, or something like that.

This is all using frobtads, which reports its version as:

FrobTADS 2.0
TADS 2 virtual machive v2.5.17
TADS 3 virtual machine v3.1.3 (mjr-T3)
FrobTADS copyright (C) 2009 Nikos Chantziaras.
TADS copyright (C) 2009 Michael J. Roberts.
FrobTADS comes with NO WARRANTY, to the extent permitted by law.
You may redistribute copies of FrobTADS under certain terms and conditions.
See the file named COPYING for more information.

So is this a bug, a misunderstanding on my part, or something more exciting and esoteric?

1 Like

Never had to use tables in that depth to find out… interesting though…

From what I’m seeing in your code, I think you might be misunderstanding the Table.

In the line above, cfg is assigned the current value of config[id] and isn’t linked to the config table at all. If id is foozle then cfg would be a copy of the LookupTable under foozle. cfg and config are two different tables at this point. You asked for a partial copy of the config LookupTable right here, so one is created.

These two lines reference or modify the Table in cfg. I’m assuming you thought the Get the player input line above updated config. It only updated the cfg table. At this point config['foozle']['value'] still equals default string.

You are correct that with how configQuestion is coded, this line would need to be uncommented. This final line of code updates the value of config['foozle'] to the modified table stored in cfg. As cfg is a local variable of this function, your changes would be lost if you didn’t do this.

cfg only ever held a piece of the config LookupTable and not a reference to the table.

Clearly. The question is, is pass by value instead of pass by reference in fact the intended behavior? The documentation doesn’t appear to indicate and if nothing else having:

        desc() { 
                local foo, bar;

                foo = config['foozle'];
                bar = foo;
                foo['value'] = 'foo';
                "Foo = <<foo['value']>>\nBar = <<bar['value']>>\n ";
        }

…and…

        desc() { 
                local foo, bar;

                foo = config['foozle'];
                bar = config['foozle'];
                foo['value'] = 'foo';
                "Foo = <<foo['value']>>\nBar = <<bar['value']>>\n ";
        }

Produce different output in a C-like language is at very least what I’d call counterintuitive.

In fact it really looks like it’s either not the intended behavior and/or it’s a bug, because it appears that what’s happening is that the entire table is replaced when it is written to, but (most) references to the old table are updated to point to the new one…so it’s basically pass by value under the hood but it’s trying to pretend to be pass by reference. An illustration:

        config = [
                'foo' -> [ 'bar' -> 'deadbeef' ],
                'baz' -> nil
        ]
        twiddle() {
                local foo;

                // This is the magic
                //config['baz'] = 'baz';

                foo = config['foo'];
                foo['bar'] = 'foozle';
                "<<foo['bar']>>\n ";
        }
        desc() {
                twiddle();
                "<<config['foo']['bar']>>\n ";
        }

This behaves as if it’s pass-by-value: as if foo = config['foo'] gets a copy of the value in the table config that is keyed by the ID foo:

>x me
foozle
deadbeef

…that is, it acts as if all the changes made in twiddle() are happening on a copy of foo in config, and a calling function doesn’t see the changes made by twiddle() after calling it because we don’t do e.g. config['foo'] = foo before returning.

But then uncomment the line marked as magic. If, in twiddle() we touch a different entry in the table before creating a variable that points to a different element in the table, the behavior completely changes. The output is now:

>x me
foozle
foozle

Now the calling function absolutely is seeing the changes twiddle() is making after it is called, which is what you’d expect if foo = config['foo'] was getting a pointer/reference to the table rather than a copy of part of it.

I just tried your code in my test environment. There is definitely a bug somewhere, probably in the VM. The way LookupTables are supposed to work, both versions should return deadbeef. I’ve used LookupTables and never triggered this bug before. It should be easy to dodge this bug. If the bug is in the VM, it probably won’t get fixed. It will just be something that needs to be taken into account. The same bug is probably in the other collection based classes as well [Vector and List].

Where’s this documented? I thought LookupTables were supposed to behave like Vectors, and Vectors are pass-by-reference.

And I just came up with a test case that strongly suggests that it’s an artifact of the inline table declaration:

        config = nil
        twiddle() {
                local foo;

                foo = config['foo'];
                foo['bar'] = 'foozle';
                "<<foo['bar']>>\n ";
        }
        desc() {
                twiddle();
                "<<config['foo']['bar']>>\n ";
        }
        initializeThing() {
                local foo;

                inherited();
                config = new LookupTable();
                foo = new LookupTable();
                foo['bar'] = 'deadbeef';
                config['foo'] = foo;
        }

…produces…

>x me
foozle
foozle

What appears to be happening the the prior examples is that something like:

        config = [ 'foo' -> [ 'bar' -> 'deadbeef' ] ]

…is creating a new LookupTable and assigning it to config['foo'] with every reference. This can be verified by doing something like:

        config = [ 'foo' -> [ 'bar' -> rand() ] ]

…which will output a new value every time you do a "<<config['foo']['bar']>>\n ".

So it looks like the real underlying problem is that inline LookupTables are borked in a very nonintuitive way.

This all really feels like TADS3 admonishing me to not use data structures; all the modern IF implementation languages appear to be mildly allergic to them.

Here’s a pair of very simple test cases that illustrate the apparent problem.

First, the inline table:

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

startRoom:      Room 'Void'
        "This is a featureless void. "
;
me:     Person
        location = startRoom
        config = [ 'foo' -> rand() ]
        desc() {
                "<<config['foo']>>\n ";
                "<<config['foo']>>\n ";
        }
;
versionInfo:    GameID
        name = 'sample'
        byline = 'nobody'
        authorEmail = 'nobody <foo@bar.com>'
        desc = '[This space intentionally left blank]'
        version = '1.0'
        IFID = '12345'
;
gameMain:       GameMainDef
        initialPlayerChar = me
;

This produces a new value every call:

>x me
425460960
-57119732

>x me
-206821450
1331245717

Second example, same syntax but in initializeThing() instead of on the property:

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

startRoom:      Room 'Void'
        "This is a featureless void. "
;
me:     Person
        location = startRoom
        config = nil
        desc() {
                "<<config['foo']>>\n ";
                "<<config['foo']>>\n ";
        }
        initializeThing() {
                inherited();
                config = [ 'foo' -> rand() ];
        }
;
versionInfo:    GameID
        name = 'sample'
        byline = 'nobody'
        authorEmail = 'nobody <foo@bar.com>'
        desc = '[This space intentionally left blank]'
        version = '1.0'
        IFID = '12345'
;
gameMain:       GameMainDef
        initialPlayerChar = me
;

This produces the same value every call (although the value is random at startup):

>x me
1960181819
1960181819

>x me
1960181819
1960181819

This very much looks like a compiler bug, or at least very peculiar undocumented behavior, to me.

1 Like

Not sure if I’m even on track with you here, but is this what I’m seeing?
In your first example config is a property whose expression is evaluated afresh each time it is accessed… the value of foo is always the result of a fresh call to rand. In initializeThing config […] is assigned a definite value… the result of a single call to rand(). From what I remember in the docs, this is supposed to be how the TADS syntax works. Tell me if I’m preaching to the choir…

So technically in your first example a new luTab is being created on every acces of config, right? Unless you say config = static new LookupTable?

Wouldn’t you need to store a function pointer in foo if you wanted it to return a rand value each time? I’m an amateur programmer so I’m just babbling…

Maybe? If it is, then it seems like a borderline misfeature, unless I’m missing something (which I suppose is possible). In what circumstances would you actually want to declare a hash table that is re-created literally every time it’s accessed?

Many places through the adv3 library there are properties defined like table = static new LookupTable, or static new Vector. There are some places in the documentation that make it really explicit that a property defined as an expression is meant to be evaluated afresh each time, unless the expression is prefixed with static…

1 Like

I guess it’s not so much about wanting to recreate a hash table each time as it is that it’s very useful and convenient to be able to freely define properties as either fixed values or expressions that return a value. As no doubt you know, prop = rand() will always come back different and prop = static rand() will always come back the same…

Okay, yeah. Looking through the adv3 source that’s certainly true. I’d (apparently foolishly) been poring over the half dozen or so official TADS3 programming references, which are coyly silent on the subject.

So yeah, it does look like that’s the intended behavior. So thanks for pointing out the examples in the adv3 source.

It does really seem like a misfeature, though. It looks like in the source the formulation is prop = static new LookupTable(), which kinda makes sense that it would create a new instance every time, because that’s what it explicitly does. But I’d expect a table declared inline to be implicitly static, same as an inline object (for example). In fact I had something like the opposite problem a couple weeks ago: an inline object declared as a property on the class is static across all instances of the class (so creating a Dresser class that had an inline ComplexComponent for a drawer creates, by default, a single drawer object that’s shared across all instances of the Drawer class). The problem (in terms of the subject of this thread) being that with objects you can use the prop: object {} form, which you can’t with LookupTable.

That turns out not to be relevant in this case, because the calls to rand() occur in the values in the table, not in the property itself. So:

        foo = static [ 'bar' -> rand() ]
        desc() {
                "<<foo['bar']>>\n ";
                "<<foo['bar']>>\n ";
        }

…will output the same value every time because the table is now mercifully static, but the rand() is only evaluated once, regardless of whether we declare the inline table as a property or in a method. It also means that identically the same code snippet without static:

        foo = [ 'bar' -> rand() ]

…will produce different behavior depending on whether it’s a property or in a method. Which seems…counterintuitive.

TADS was my first introduction to the programming world, since then I’ve dabbled a little bit with C++ and Python. So I can see where it might seem counterintuitive; I’m just saying that the docs did make it clear that’s how it works. In a method, the equals sign is assignment of the right expression’s evaluation. In a property context, the equals sign is not assignment: it’s shorthand for a method return. canPass = true actually means ‘return true’, in the same way that canPass = actor.isHolding(charm) will evaluate whether the actor has it on each new call. So even though it may seem odd, in order to be consistent, prop = LookupTable evaluates the new() each time unless static is used.
Or at least, that’s my best understanding…

Still wasn’t sure if you were trying to get foo to yield a new value within the static table, or wanted tab[foo] to be assigned a random value just upon creation?

And of course the prop: object {} does not work this way, it’s not a method return shorthand the way the equals sign is…

I think I understand the point you’re trying to make, but that’s always what the assignment operator does. In a lot of languages (like, for example, JavaScript) there are explicit getter and setter methods which make this a little more syntactically obvious, but that’s true in almost all nontrivial programming languages.

But my complaint isn’t about the syntax of the assignment operator, it’s with the implementation of the inline LookupTable declaration. My point is that foo = [ 'bar' -> 'string literal' ] ought to be parsed as a static declaration by default the same as foo = 'string literal' or foo = 5 are. The alternative–re-creating the hash table every time it’s referenced–doesn’t make much sense at all, and really doesn’t make sense as a default. I mean you pointed out the adv3 source. Have you seen a single instance in which a hash table is declared as a property and it isn’t declared static?

Anyway, the point is probably largely academic. I’m not sure if TADS3 (the language itself) is still under active development at all, and even if it is I know I’m not and I gather you are not developers for it, so it’s not really an issue we’ll be able to resolve among ourselves.

Historically the TADS assignment operator was Pascal-like (:=). Later, C-style assignment (=) was permitted. (I think there was a compiler switch for selecting between the two, I’m too lazy to look it up.) I believe users back then were complaining about the Pascal-style assignment format, hence the option was made available.

When TADS 3 came along, MJR decided it was too confusing to have multiple operators, and landed on the C-style for the sake of familiarity. Yes, that conflicts with the declaration symbol, but changing both was probably seen as too onerous for porting code.

(It’s too bad the declaration symbol wasn’t made into :, that would have made inline object syntax naturally work w/o special-casing them.)

1 Like