ClassX.copy() incorrectly copies properties and attributes not associated with ClassX?

The behavior of the .copy() method of classes seen in Inform 6.31 and Inform 6.33 does not seem to match the description given on DM4 p. 66 (online: DM4 §3: Objects and classes ), which seems to indicate that only properties (and possibly only attributes relevant to the class for which the method is invoked) will be copied. Specifically, the passage:

It’s rather useful that recreate and copy can be sent for any instances, not just instances which have previously been created. For example, Plant.copy(Gilded_Branch, Poison_Ivy) copies over all the features of a Plant from Poison_Ivy to Gilded_Branch, but leaves any other properties and attributes of the gilded branch alone.

However, it appears that this is not the entire story. In the following test code, two classes (SmallContainer and Battery) are set up that both derive from a third (Treasure):

Demonstration Code
Constant Story "Copy Oddness";
Constant Headline "^Unexpected changes to attributes and properties when using .copy()?^";

Include "Parser";
Include "VerbLib";
Include "Grammar";

Class Room
    has light;

Class Treasure
    with value   5;

Class SmallContainer
    class Treasure
    with    capacity    20
    has container open ~openable ~lockable ~locked ~transparent ~enterable; 

Class Battery
    class Treasure
    with    capacity    50;

Battery battery1 "platinum fuel cell" selfobj
    with    name 'platinum' 'fuel' 'cell' 'battery',
            before
                [;
                    Wave:
                        Treasure.copy(self, golden_bucket);
                ]
    has transparent;

Room Start "Starting Point"
    with    description
                "An uninteresting room.";

SmallContainer golden_bucket "golden bucket" Start
    with    name 'golden' 'gold' 'bucket';

Verb    'squint'
    * 'at' noun -> Squint;

[ SquintSub ;
    print "^============================================";
    print "^", (name) noun;
    print "^container:", (TorF) (noun has container);
    print "^lockable:", (TorF) (noun has lockable);
    print "^locked:", (TorF) (noun has locked);
    print "^open:", (TorF) (noun has open);
    print "^openable:", (TorF) (noun has openable);
    print "^transparent:", (TorF) (noun has transparent);
    print "^value:", noun.value;
    print "^capacity:", noun.capacity;
    print "^ofclass Treasure: ", (TorF) (noun ofclass Treasure);
    print "^ofclass SmallContainer: ", (TorF) (noun ofclass SmallContainer);
    print "^ofclass Battery: ", (TorF) (noun ofclass Battery);
    print "^============================================^";
];

[ TorF p ;
    if (p)
        print "TRUE";
    else
        print "FALSE";
];

[ Initialise ;

    location = Start;

];

A call to Treasure.copy(BatteryInstance, SmallContainerInstance) seems to do the following:

  1. copy the value of the .capacity property, despite the fact that this property is not declared in Treasure and despite the fact that Battery has its own declaration
  2. copy the (set) values of the container and open attributes, despite the fact that these attributes are not set for Treasure
  3. copy the (unset) value of the transparent attribute, despite the fact that this attribute is not set for Treasure and that the specific instance is declared with that attribute set

> SQUINT AT CELL

============================================
platinum fuel cell
container:FALSE
lockable:FALSE
locked:FALSE
open:FALSE
openable:FALSE
transparent:TRUE
value:5
capacity:50
ofclass Treasure: TRUE
ofclass SmallContainer: FALSE
ofclass Battery: TRUE
============================================

> WAVE IT
You look ridiculous waving the platinum fuel cell.

> SQUINT AT CELL

============================================
platinum fuel cell
container:TRUE*
lockable:FALSE
locked:FALSE
open:TRUE*
openable:FALSE
transparent:FALSE*
value:5
capacity:20*
ofclass Treasure: TRUE
ofclass SmallContainer: FALSE
ofclass Battery: TRUE
============================================

Is the DM4 simply in error about the way that this method works, or has the way it works been changed since the time of original publication? If so, is there a more detailed description of the actual current function elsewhere?

2 Likes

OK, based on experimentation in 6.31, this is the behavior that seems to actually occur given a ClassX that is inherited by objects source and target when ClassX.copy(target, source) is called:

  1. The current state of all attributes are copied from source to target.
  2. Every named property is copied from source to target regardless of origin, with the following exceptions:
    a) if source has a named property that target does not, the property is skipped
    b) if target has a named property that source does not, it is left alone
    c) if source and target share a named property that is read-only (via standalone Property declaration) for either, it is left alone (so source's read-only value won’t be copied even when target's value is writable)
    d) if source and target have a named property that holds a property array, it is copied if and only if both objects have the same number of entries for that property (i.e. .# operator returns same number), otherwise it is left alone

Is this the complete picture?

I have never dug into this code, other than porting it to Glulx in 1998-whenever that was.

I’m sure there’s never been an intention to change the behavior since I6 came out. (There may have been bug fixes in the early years.)

This doesn’t surprise me. The VM (both VMs) have no way to distinguish “unspecified attribute” from “attribute explicitly set false by this class or object.” They’re both just zero bits.

I found the relevant routine in the veneer code. I looked at the 6.34 version, and, if I’m reading it correctly, it basically conforms with the rules that I laid out above.

The Inform Technical Manual does list a couple of bug fixes, in the 6.15 to 6.20 era. Presumably the code now is working as intended, DM4 claims notwithstanding.

It looks like there is sufficient information available at run-time to do a better job of deciding which values should be considered relevant during a call to a class’s .copy() method:

  • A class’s attribute settings are stored in a “secret” location following its default common property table. Those attributes that are set “on” can be deemed the only ones relevant to the class.
  • A class’s common and individual properties are also stored in the story file. These can be accessed using the same method as is used by the superclass operator. Those properties with entries in the common property table and individual property table of a class can be deemed relevant to the class.

After identifying what’s considered relevant to the class, the target can be set as follows:

  1. If the attribute or property is relevant to the invoked class, overwrite the value in the target object with the value from the source object.
  2. If the attribute or property is not relevant to the invoked class, leave its value in the target object in its current state.

It therefore seems that the .copy() method can be made to work as described in DM4, i.e.

“[copying] over all the features of a [given class] from [a source object] to [a target object], but [leaving] any other properties and attributes of the [target object] alone.”

Is there any good reason not to do this?

We try not to change the code generation for valid source code. This includes changing the veneer. We’ve done it, but it requires a good reason.

Adding code to the veneer requires a better reason, as this might push a tightly-contructed game over its size limit.

Inform’s inheritance behavior and its create/recreate/copy methods are not in line with the manual. This is good to know and I’m glad you’ve turned it up. However, I’d rather leave it as-is and document what the compiler does. Because:

  • Existing code might depend on the current behavior.
  • The current behavior may follow a consistent model, even if it’s never been correctly documented. (Graham is pretty good about consistent code.) Changing one part might make the behavior more confusing, and changing everything could be a large amount of work.
  • Increasing the size of the veneer code is a bad idea.

Really I doubt that existing code depends on the current behavior. Multiple inheritance and cross-class copying just aren’t good fits for IF design. It took twenty years for anyone to notice these problems, and it wasn’t in the course of writing a game! So my real argument is that these features are not very important and it’s not worth the hassle of fixing them.

2 Likes

I’ve written a prototype of this myself in the interim. The change is not gigantic.

I don’t understand your assertion that multiple inheritance is not a good fit for IF – will you elaborate? From my perspective it seems like a potentially very useful method of design, allowing one to bundle various aspects of objects’ features and behavior into a discrete and reusable package. However, that viewpoint depends on it working as described in the manual. In its current form I agree that it does not seem very useful at all.

@zarf: I’ve been contemplating your answer above. I would still like to better understand why you believe that this feature is “not very important.” The theoretical feature is pointed out relatively early in DM4 (p. 66), where it gets a special note section in which Professor Nelson calls it “rather useful” as a feature and lays out an example claiming to demonstrate that utility. While I appreciate that the existing code base has practical precendence over the explanatory text of the designer’s manual, it certainly seems as though the aspirational vision is that the feature would work as described in DM4.

It seems circular in nature to assert that the feature is not important because nobody has successfully used it in the manner prescribed by DM4; it’s a bit like saying that a collapsed bridge isn’t important because nobody ever uses it. Obviously, it would not be possible to do so given the current code base.