These violent delights

Tuesday, February 22, 2022

Comments: 4   (latest March 16)

Tagged: adventure, bugs, code, inform 6, debugging, compilers, violence

I've spent the past few days working on an Inform 6 compiler patch to tighten up generated code. While I was working, I tweeted this screenshot of a buggy game:


I feel silly posting an image of text, so here's that again:

 At End Of Road                                      Violence isn'Violence isn't
tease oti n.


Welcome to Adventure!

Violence isn't the answer to this one.Violence isn't the answer to this
one.Release 5 / Serial number 961209 / Inform v6.37 Library Violence isn't the
answer to this one. S

At End Of Road
Violence isn't the answer to this one.

>east

Inside Building
Violence isn't the answer to this one.

Violence isn't the answer to this one.

Violence isn't the answer to this one.

Violence isn't the answer to this one.

Violence isn't the answer to this one.

>

I tweeted it because it was funny; my followers volubly agreed. (Thanks folks!) But how did this happen? Hey, I haven't written a code post in a while. Let's dig into it.


My patch was intended to strip out dead code. What does this mean? Well, say you write an Inform 6 routine like this:

[ Func;
    if (false) {
        print "Goodbye cruel world.";
        return;
    }
    print "Hello.";
];

You may not know I6, but you can probably tell that this function just prints "Hello." The if statement is always false and never happens. But the Inform compiler was compiling the first print and return statements anyway. I wanted to avoid wasting that space.

In this simple example, dead-code stripping is easy. In fact the I6 compiler was already detecting the dead if statement; it just needed a nudge to skip generating code for it. However, more complicated code samples were, well, more complicated, and I spent a few days massaging the algorithms to get it all right.

The crux of these changes was a flag called execution_never_reaches_here, which means exactly what it says, because Graham Nelson likes self-documenting code. If that flag is set, you can skip generating output. The fun part is making sure the flag gets turned on and off at all the right times as you compile your function.

(Okay, execution_never_reaches_here doesn't mean exactly what it says, because I've stuffed some bit flags into it. I won't get into why. Not relevant to this story.)

Now, there's another step to this patch. In Z-code (and Glulx, but we're testing Z-code here), strings and code are stored separately in memory. I'd gotten the compiler to skip generating the print and return statements, but the string "Goodbye cruel world." was still being stored in string memory.

So I went into the compiler's compile_string() function and added a couple of lines pretty much like this:

if (execution_never_reaches_here) {
    return 0;
}

The point of this change is that if your print statement isn't compiling into code, who cares about the string that's being printed? compile_string() has to return something, because its type is int. but the value returned will be dropped on the floor. So we might as well return zero.

So that's easy, right? Three lines (okay, eight lines with comments and another check which isn't important right now). Compile the compiler, compile Advent.inf, test.

Welcome to Adventure!

Violence isn't the answer to this one.Violence isn't the answer to this
one.Release 5 / Serial number 961209 / Inform v6.37 Library Violence isn't the
answer to this one. S

What? What?

If you're really in tune, you may already have guessed my mistake. If not, I'll draw out the suspense by one more paragraph break...

...I spent all that time making sure that execution_never_reaches_here was handled correctly inside a function. But I forgot to check what happened to it between functions. In fact I had written some cleanup code to set execution_never_reaches_here = -1 at the end of each function! And then forgot I had done that.

Of course the value -1 is considered true, or truthy, as we say. So whenever the compiler ran into a string that was outside a function -- say a global constant, or part of an object -- it obligingly skipped over it and returned zero.

The fix was easy: just clean up execution_never_reaches_here to be zero (false) between functions, instead of -1. Then everything works right.


But wait. I haven't explained anything! Why does skipping strings cause the game to print "Violence isn't the answer to this one" all over the place?

That line is familiar to any Inform user. It's probably the most recognizable message in the Inform library. It's what happens when you ATTACK or BREAK something that the game hasn't defined as breakable. (Or if you HIT it, or FIGHT, KILL, MURDER, SMASH, WRECK... A lot of synonyms.)

Library default messages are a subtle art. You need a response which is appropriate to anything the player might try to BREAK -- in any game, including ones you haven't imagined.

This is tricky! Say the library default was "That's unbreakable." This would be appropriate for BREAK STATUE, but it reads off-key for FIGHT FROG -- it's not obvious how a frog can be unbreakable. And for SMASH WINEGLASS it's outright confusing. (Yes, the author should write a better response for SMASH WINEGLASS. But it's the library's job to provide something adequate if they don't.)

"Nothing obvious happens" isn't bad, but it's not great either. It implies that the player attempted violence, but failed to make a dent. Again, for fragile objects, this is misleading. You can get away with "Nothing happens" for PUSH actions, since pushing is implicitly... kind of boring. (Maybe the object rolls freely; maybe it's fixed in place. You can describe both outcomes as "nothing happens.") But violence really should have a visible effect in some cases.

What we want is a response which implies that the player decided not to do that, and moreover should try something else, because no ATTACK action has been implemented. But we can't say "Violence isn't the answer," because something else in the game might be genuinely attackable! (Zork, after all, has just three FIGHTable monsters -- but you can't win if you never fight.) We have to say "Violence isn't the answer to this one," and leave the player's options open.

So "Violence isn't the answer to this one" is a carefully-tuned compromise. It's held up very well over the thirty years of Inform's history.

(The final rule of library defaults is that they should be neutral and unassuming in tone. "Violence isn't the answer to this one" fails this spectacularly -- it's way too memorable! But we have to allow Graham Nelson a few opinionated moments, and anyway my bug wouldn't have been funny without it. Remember my bug? This is a song about my bug.)


Recall my mistake. I was omitting certain strings from the game -- all strings that occur outside functions -- and instead recording their value as zero.

It's clear, looking at the screenshot, that "Welcome to Adventure!" occurs inside a function. So do phrases like "Release" and "Serial number" and "Library". But the library version must be a string constant. The same goes for room and object descriptions. Those get zeroed. And for some reason, a zero value gets printed as "Violence isn't the answer to this one."

(Room names like "Inside Building" and "At End Of Road" are actually outside functions too, but the way they're compiled means they come through in the clear. I'm not getting into all the details.)

The status line is particularly messy. It turns out the game is trying to print "Score:" and "Moves:" in the upper right. Those overflow the status line and go completely wrong.

The mix of right and wrong text can get even sillier. At one point I got it to say:

You can also see Violence isn't the answer to this one.set of keysViolence isn't
the answer to this one.Violence isn't the answer to this one. tasty foodViolence
isn't the answer to this one.Violence isn't the answer to this one.small bottle
(in Violence isn't the answer to this one.Violence isn't the answer to this
one.Violence isn't the answer to this one. bottled water) here.

...which looks indescribable until you realize that the game is stitching together a list with string constants like "a" and "some". The object names are fine; the short words get replaced.

So that all makes sense... except we still haven't explained how zero turns into, you know, violence.

You might guess that "Violence isn't the answer to this one" is the first, or rather zeroth, string in the game's string table. And you're right!

But how did it get there? It's certainly not alphabetically first. It's not the first string constant defined in the game. It's not the first string defined in the English.h library file. It's not even the first string defined in the LanguageLM() function in that file.

This puzzled me severely... until I remembered a quirk of the I6 compiler. (It has many quirks.) In a print statement, short strings are compiled inline in the function code. (Z-code allows this.) Longer strings -- anything over 32 characters -- go into the string table.

I'm pretty sure this rule was a hacky way to keep function size down. It's hard to generate code for very long functions, because Z-machine branch opcodes have limited range. Game authors usually don't write very long functions -- but they do sometimes write functions which print a lot of text. If all of that text gets inlined, the function gets too long to manage. So the compiler got tweaked to move larger blocks of text to the string table, which doesn't have the same size limits.

So that's the last link in our debugging chain. "Violence isn't the answer to this one" isn't the first string in the game -- but it is the first long string in the library which appears in a function. Short strings get inlined; strings outside of functions get dropped. The library is compiled before the game. So "Violence isn't the answer to this one" is the first string in the game which makes it into the string table. That makes it the zeroth string entry... and everything else follows.

Whew!


What's impressive about this bug, really, is that it's good comedy.

"Welcome to Adventure" is an instantly recognizable hook (to you and me). Then you spot "At End of Road" and "Inside Building", which confirms what you're looking at -- after you think about it for a second. But it's full of repetitions of the "Violence" line, which is also recognizable, but wrong. The I6 header block is familiar, but it's broken up. You consider the theory that the "violence" line has been stuck in random places. But then the status line (familiar!) breaks the pattern again with partial words, and then that "tease oti n". By this point you're completely at sea.

(You gotta break up the "spam spam spam" chant with "spammity-spam, wonderful spam" sometimes. Same idea.)

Honestly, it's exactly the right balance of Adventure text and "Violence isn't the answer to this one." If I were hand-tuning it I might stick "shiny brass lamp" somewhere in the last five lines, just to trip people up one more time. But it doesn't need it.

And I swear this was all entirely accidental. I didn't do a thing except type EAST. Sometimes the universe just hands it to you.

(Thanks to @ddfreyne for the reply which turned into the title of this post.)


Comments imported from Blogger