§1. Undocumented features of Inform are no longer very secret (here you are reading about them, after all), but they can be hard to find by raking through the source code at random. What we mean by "undocumented features" here are capabilities intentionally included in Inform, but not mentioned in any of the user-facing documentation. That documentation used to be just the two books built in to the apps (Writing with Inform and The Recipe Book), but those are now supplemented by the manuals for the three command-line tools inform7, inbuild and inter, which cover many features invisible from within the app and at present available only for command-line users.
This page covers a handful of other features still, but which are intended mainly for maintainers of Inform and may change at any time without notice.
§2. In working out what the compiler is doing (or has just done), the debugging log is always the first thing to investigate. This a text file like a running journal of what the compiler is doing; if it halts partway, either with problem messages or an internal error, then the log is still written up to that point, and can be revealing. The log can be viewed in the Inform apps, but usually only if an "advanced" preference setting has been ticked. Once that is done, the log appears as one of the tabs in the Results pane after each compilation (successful or not). A similar preference setting enables the Inform 6 output to be viewed, too, though of course only if the compiler gets that far.
The compiler is more instrumented than a first look at the log might suggest. Most of its "debugging aspects" are switched off by default: if they were all switched on, the log would be simply enormous. Each aspect has a textual name — for example, "predicate calculus" shows the logical propositions formed and simplified inside the compiler. (On each run, the bottom of the debugging log lists the range of available aspects.) There are four ways to change what aspects are switched on:
- (1) Add a sentence like "Include predicate calculus in the debugging log." to the source text for the project. For obvious reasons, this can only take effect after sentences begin to be understood by the compiler, but in practice that's early enough to be useful.
- (2) Add a command-line switch like -log predicate-calculus (note that any spaces in the name are replaced by hyphens) when running inform7 from the command line. -log list prints a list of the aspects.
- (3) If you're interested in how Inform is reading an assertion sentence (say, "A ball is on the oak table"), try placing the special sentences *. and *. either side of it. This switches on, and then off again, a convenient mode of logging which shows (i) the primary verb, (ii) the full sentence diagram, (iii) any logical propositions derived from that, and (iv) any inferences drawn as a result. The log output is mostly self-explanatory, but the notation X =14 Y (or some other number) means that the assertion-maker is coupling the tree nodes X and Y together in Case 14 of the Assertions (in assertions) algorithm. For example, in the log output from the following, you'll see [ball/PROPER_NOUN_NT] =36 [is on/RELATIONSHIP_NT]:
The French Kitchen is a room. An oak table is in the Kitchen. *. A ball is on the oak table. *.
- (4) If you're interested in how Inform is reading a phrase inside a rule or phrase definition, and particularly in the running of the Dash typechecker, try placing the special phrases ***; and ***; either side of whatever phrase you're interested in. This shows exactly what code path through Dash is being followed, and which kind conformances are being checked. For example:
To consider (N - a number): ***; showme N times N; ***.
- (5) It is also possible to write *** "predicate calculus"; to switch on just a single named debugging aspect, so:
Every turn: *** "predicate calculus"; now the rock is in the bucket; ***.
§3. Inform compiles first to an intermediate representation called Inter, and only then compiles that in turn to its actual output. The Inter produced is then discarded and normally never leaves memory, so it's invisible to the user. When debugging, though, we sometimes want to see what it was.
Two special debugging aspects are provided for this.
- ● "Include Inter in the debugging log." will cause Inform (strictly speaking, inter) to write out the entire compiled Inter tree, in its textual form, into the debugging log. Be warned that this is an awful lot of output, and will cause a pause of an extra couple of seconds just to write.
- ● "Include Inform Inter in the debugging log." will cause only the Inter generated by Inform itself to be logged. Logged earlier in the run, this is a smaller tree not yet containing material from kits such as BasicInformKit or WorldModelKit. (Smaller, yes: small, no.)
- ● If you're interested only in the code for a single phrase or rule, place the pseudo-phrase *** "inter"; somewhere (anywhere — makes no difference) inside its body. Note that this shows the Inter generated by the whole definition, and of course that never happens if problem messages are generated instead. For example: Every turn: *** "inter"; now the rock is in the bucket.
§4. The "Test... with... " feature of Inform is familiar to most users, since it's used by almost all of the examples, usually with source text like:
Test me with "examine box / take box".
Less well-known is that it can also run unit tests built in to the compiler. For example:
Test description (internal) with a number which is even.
The test here is called description. The marker (internal) is what causes this sentence to be read as an internal test case. Most tests, description among them, then expect some details to follow the word with. In this case, it's a description, "a number which is even", and what the test does is to convert this to a logical proposition and print that proposition out.
What happens is not that inform7 prints the test output to the console, or even to the debugging log. It compiles the test output into the story file it is generating, so that this is printed out when the story file runs. That may sound needlessly indirect, but it's convenient both in the app and also for intest to check. Add the above test sentence to some project, click Go in the app and you see the result.
- ● Test description (internal) converts a description such as "a number which is even" to a proposition, showing the kinds of its variables and whether they are free or bound:
1. a number which is even << number(x) & even(x) >> x (free) - number.
- ● Test eps (internal) tests just the EPS-file-maker part of the spatial map index, and therefore isn't very useful.
- ● Test evaluation (internal) is used for verifying that literal arithmetic quantities written in unusual notations are evaluated correctly. For example, Test evaluation (internal) with 1 sq m divided by 20cm.
- ● Test headline (internal) is not a test at all! It simply prints out a dividing subheading in the test output — if a project makes hundreds of internal tests then these subheadings make the output much more legible. For example, Test headline (internal) with Unquantified relative clauses. puts the subheading "Unquantified relative clauses" at this point in the test output.
- ● Test index (internal) produces just one element of the index for test purposes, as a chunk of HTML. For example, Test index (internal) with Mp. tests the Mp (spatial map) element.
- ● Test kind (internal) is not much use, but can produce four different listings of the kinds known to Inform, according to whether the with ... text is 1, 2, 3 or 4.
- ● Test pattern (internal) is for testing that the rather complex grammar used for "action patterns" in Inform is being parsed correctly. It has two different forms: if the with ... text begins with list then a list of actions is expected, and if not, then the text should describe a single action (though possibly quite vaguely). For example:
Test pattern (internal) with asking Hans to try taking the counter. Test pattern (internal) with list looking or taking inventory in the presence of Hans in the Laboratory.
- ● Test refinery (internal) shows how assertion sentences are "refined". For example, Test refinery (internal) with Jane is a woman.
- ● Test sentence (internal) is very like description, but expects a whole sentence, that is, something with no free variables rather than one:
1. everyone likes Peter << ForAll x IN< person(x) IN> : amity(x, 'peter') >> x - object.
§5. inform7 has a test suite of roughly 2000 test cases, some of which are large and elaborate. Running through this test suite must be done at the command line (indeed, the tests are not shipped inside the apps, only as part of the inform Git repository). The intest tool is provided for this, and reading the intest manual is a good place to begin. A test suite to be operated by intest is configured by a *.intest file, and the full details of how the I7 suite works are laid out in inform/inform7/Tests/inform7.intest. About 25% of the tests are the Examples from the manuals; another 25% more or less systematically poke at language features, and edge cases which proved tricky in the past; and the remaining 50% test that the compiler produces correct problem messages.
Tests in the suite can be divided up into the following categories:
- ● Those in inform/inform7/Tests/Test Basic are Basic Inform tests, run through the compiler in -basic mode, meaning that the language features for interactive fiction, the command parser and the world model are all missing. (These tests run relatively quickly as a result.) With no command parser loop, such programs run by printing some output and stopping.
- ● Those in inform/inform7/Tests/Test Cases are standard tests of Inform not related to Examples in the documentation. This is a mixed bag which has accumulated over the years.
- ● Those in inform/resources/Documentation/Examples are the examples from the manuals — this makes up about 25% of the whole suite, and in practice tests interactive-fiction features of Inform pretty systematically, besides ensuring that the examples in the manual do what we claim that they do. Note that the name of an example shown in the manual may not be the same as its Intest case name: thus, "Bruneseau's Journey" has test case name Candle. But you can use intest's -find feature if stuck:
$ ../intest/Tangled/intest inform7 -find Bruneseau Test cases matching 'Bruneseau': Candle = Bruneseau's Journey
- ● A few test cases are extracted and run from the extension documentation for extensions supplied with Inform, such as "Locksmith".
- ● Those in inform/inform7/Tests/Test Internals run internal test cases (see above).
- ● Those in inform/inform7/Tests/Test Makes are a small number of demonstration programs which are hybrids running partly in C, partly in Inform. These are built using individual makefiles — hence the name. The test cases correspond to the example projects in the documentation on this: see Calling Inform from C (in inform7).
- ● Those in inform/inform7/Tests/Test Problems are Inform source texts expected to cause the compiler to halt with a problem message: they check that it does in fact do so, and that the problem is correctly worded. See below.
- ● Those in inform/inform7/Tests/Test Releases are for testing the release instructions which inform7 generates and supplies to inblorb. (Note that inblorb itself is not run in these tests: only the instructions are checked. But inblorb has its own test suite, in which of course it is run.)
§6. Each individual test has a name. The following naming conventions apply:
- ● No two different cases have the same name.
- ● If a test case name ends in -G, it will be compiled to Glulx and run through the dumb-glulxe interpreter.
- ● If it ends -C, it will be compiled to ANSI C, then run through a C compiler to produce a stand-alone executable, and that is what will then be run.
- ● If the name has neither of these endings, it will be compiled to the Z-machine and run through the dumb-frotz interpreter.
- ● Names beginning BIP- are Basic Inform tests which systematically verify that language features work. These tests mostly come in all three flavours, so for example BIP-Let, BIP-Let-G and BIP-Let-C check the same functionality when code-generating for the Z-machine, for Glulx and for C. (In a few cases to do with real arithmetic, there's no Z-machine test.)
- ● Names beginning PM_ are tests of problem messages: for example, PM_ConflictedReturnKinds or PM_PropertiesEquated. Almost every I7 problem message has a code-name like this, and a corresponding test case. Though these code-names are not displayed on the Results page shown in the Inform app when a misbehaving project generates these problems, the code-names of any issued problem messages are listed in the debugging log, which also gives a file and line number reference to the Inform source code showing where the problem was generated.
§7. The complete test suite takes a long time to run — typically 10 to 11 minutes on the author's computer in 2022, even running 16 simultaneous tests on different cores. It's sometimes more practical to run subsets of the suite. For example, if debugging the part of Inform which generates C code from Inter, there's no point verifying the problem messages or the 900 or so tests which compile to Z or Glulx.
- ● The Intest keywords cases, examples, and problems catch just the Test Cases, document examples, and Test Problems cases respectively.
- ● There are also "test groups" stored in inform/inform7/Tests/Groups. See the intest documentation for how to set up these groups, though they're pretty self-explanatory.
For example, these three commands:
$ ../intest/Tangled/intest inform7 all $ ../intest/Tangled/intest inform7 problems $ ../intest/Tangled/intest inform7 :calculus
run the full test suite, just the Test Problems cases, and just the :calculus group respectively. (Note the colon.) Particularly useful groups include :c, which performs every test involving C output, and :basic, which performs the whole BIP sub-suite.
§8. Internal errors in inform7 generate the infamous "abject failure" problem message. These are essentially failed assertions in the compiler, but they cause a clean termination of the process with exit code 1, just as any more orthodox problem message would. A Results page in HTML is generated normally. As a result, they're no harder for the GUI apps to deal with than any other sort of problem would be.
Harder to handle is an actual crash of the inform7 executable, in which an abrupt exit occurs where no problem message is generated: say if it dereferences a null pointer, or divides by zero. This of course should never happen, but we want the GUI apps to be able to cope if it does. And in order to test that, we give inform7 the ability to simulate such crashes.
This is done with three words too unspeakable to write, the "secret hieroglyphs of dread power". They're actually not much to look at. The first is:
ni--crash--1 is a room.
This crashes out with exit code 1. The second and third are much the same, but are written ni--crash--10 and ni--crash--11, giving exit codes 10 and 11. (On MacOS, at least, these are what result from derefencing a bad pointer or overflowing the stack through infinite recursion.)
§9. If Inform does crash, or throw an internal error, or even simply produce an unwanted problem message, but in circumstances which are unclear, then it's useful to see a stack backtrace at the point where the error came to light. This is where Intest's -debug feature is useful. For example:
$ ../intest/Tangled/intest inform7 -debug Acidity
This runs the test Acidity, but with the inform7 executable running in the lldb debugger (on MacOS, anyway), and moreover with it set to deliberately cause a division by zero after any problem message is generated. This forces inform7 to exit into the debugger, at which point a stack backtrace can be seen (in lldb, anyway) by typing bt.
Of course, Inform is unlikely to crash on any of the tests in the regular test suite: more likely you have some wacky source text causing a crash or unwanted problem message, but which isn't in the suite. In that case, though, you just need to create a temporary test case for it — the author likes to keep this as the file inform/inform7/Tests/Test Cases/temp.txt, and then:
$ ../intest/Tangled/intest inform7 -debug temp
does the trick. But be sure not to add this temporary file to the repository.