Compiler ignoring redefinition of constants in some circumstances?

As seen in a discussion related to I7, there appears to be an issue with the way that switch clauses which make use of constant values are compiled. If a constant that is defined after the switch block is used as the trigger for a clause, but then that constant is then redefined via use of #Undef followed by a new declaration, then it is the first value of the constant that governs the behavior of the switch block, not the final value of the constant.

Demonstration code follows:

Redefined Constant Issue
Constant Story "Redefined Constant Issue";
Constant Headline "^a bug demonstration^";

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

Class Room
    has light;

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

[ DemonstrateIssue n ;
    switch (n) {
        badconstant: print "matches badconstant";  ! compiled with hardcoded comparison to 3?
        default:     print "does not match badconstant";
    }
];

Constant badconstant = 3; ! value used when compiling DemonstrateIssue()?
#Undef badconstant;
Constant badconstant = 2; ! value ignored when compiling DemonstrateIssue()?

[ Initialise ;

    location = Start;

    print "badconstant = ", badconstant, "^"; ! expected to be 2, and is
    print "parameter = 2: ", (DemonstrateIssue) 2, "^"; ! expected to matches, but doesn't
    print "parameter = 3: ", (DemonstrateIssue) 3, "^"; ! expected not to match, but does
    print "parameter = badconstant: ", (DemonstrateIssue) badconstant, "^"; ! doesn't match

];

This is observed in Inform 6.33, 6.34, 6.35 and 6.36 (as of a few weeks ago).

That’s the way redefined constants work. The new value only applies after the redefinition.

This isn’t very consistent with Inform’s “constants can be defined anywhere” policy. But Undef was an afterthought. I was imagining using it to adjust #ifdefs (which do care about definition ordering) rather than code.

1 Like

So… the #Undef directive effectively segments the scope of the symbol table during compilation?

Would it be possible to add a #Redefine directive or the like that allows for global redefinition of a constant without segmenting the scope of the symbol table (i.e. last redefinition prevails globally)? Right now #Undef appears to be the only tool for redefining a constant found in an included file or earlier in the code.

Short answer: no.

The compiler compiles numeric constants as their values, unless they haven’t been defined yet, in which case it leaves a “backpatch” record and fixes them up later.

String and function addresses are always backpatched, because the compiler doesn’t know their numeric value until the end of compilation.

However, in a many contexts, it doesn’t make sense to use a backpatch record. The obvious example is #iftrue CONST == 0; the compiler has to know then what the value is so that it can decide whether to continue compiling lines. Array arr --> CONST; is another example that requires a known numeric constant. There are lots of others.

(If you try using a forward-declared constant or function address in these situations, you’ll get an error like “Expected constant but found expression.”)

You’re suggesting using backpatch records for all constants, so that they can be filled in with the last defined value. This is impossible; it breaks all the cases I just mentioned.

We could in theory have a declaration like SoftConstant foo = 1; which creates a constant that is always backpatched and can be redefined any number of times. But I think that’s way more confusing than the concept is worth. Use a global variable or an array or something.

Again, Undef was a late addition and primarily intended for semi-clever #if tricks. (Tricks borrowed from C, whose preprocessor never looks forward in the source code.)

2 Likes

Thank you for explaining this. I have been thinking about it.

It seems like there are at least three separate contexts for use of a constant during compilation:

  1. Use in a preprocessing directive, which, as you point out, must be evaluatable when deciding what becomes part of to-be-compiled source code, and which must take into account changes in the constant’s definition at various points in the source in response to use of #Undef and possible subsequent redeclaration.

  2. Use in an I6 routine, which must decide on the value(s) of any constant(s) involved prior to generating VM instructions. I had thought that the compiler already uses backpatching for this when it encounters undeclared constants, so the question is at what point does it apply that backpatching (and therefore what value of the constant is used when doing so). Existing behavior implies that perhaps it performs a backpatching cycle whenever it encounters #Undef? I haven’t dug into the compiler enough to understand the interactions at play, but it certainly seems like the compiler could defer its decision on what value to use when backpatching until it reaches the “final” version as evaluated at the end of source code.

  3. Use in defining I6 arrays, which must decide on value(s) of any constant(s) involved prior to laying out the virtual machine’s memory. Again, I don’t understand the complexities at play during the compilation process, but if memory layout happens after backpatching, it again seems like it would be possible to defer to the value of the constant as evaluated at end of source. [I see that as of 6.34, the compiler treats use of an undeclared constant for an array size value as a fatal error (“Array sizes must be known now, not defined later”), while 6.33 does not object. The release notes for 6.34 don’t seem to mention this; perhaps I’m missing it?]

My thinking here is that, in a case where I want to use a library file (somebody else’s or my own) but with a modification to a constant value, my intuition is that if I #Undef and then redeclare a constant in my main file, that redefinition should apply to all uses of the constant in code compiled from the library. That’s a focus on cases 2 and 3 above.

Maybe that’s a bad intuition, and it’s clear that case 1 has its own separate needs, but I wonder if the two types can’t safely coexist, i.e. for preprocessor statements to use the constant’s value at that point in source and for regular statements to use the constant’s value at end of source.

Memory layout happens before backpatching.

6.33 happily uses an undefined value as the array length without generating an error. (I think the array length winds up being set to an internal constant index, rather than the final defined constant value.)

In general terms, when using conditional compiling, #undef is the reciprocal of #define (or whatever is named in the language), so the redefined constant example given by Otis should be along the line:

(note carefully that this below is pseudocode, modeled on c/cpp)

#define VALUE 3
Constant badconstant = VALUE
#undef VALUE
#define VALUE 2
Constant badconstant = VALUE

… this, of course, is the general theory.

Best regards from Italy,
dott. Piergiorgio.

1 Like