Inform 6 template language, or I6T for short, is a notation for expressing low-level code in Inter.


§1. Three readers. In the pre-2015 design of Inform, the I6T interpreter was a formidably complex function. There was a special Main.i6t file which contained, essentially, the entire top-level logic of the compiler, calling hundreds of different functions (a design pattern recommended by Eric Raymond's "The Art of Unix Programming", but not in fact helpful in practice). There were numerous features for having template files open each other, and switch output on and off: these were needed when Inform was reading raw I6T versions of the low-level library code on every compilation, but in the age of Inter, this no longer happens. (See the assimilator in the code-generator for what is done instead.) Finally, I6T was complex because it had subsumed three quite different functions.

Those three usages live on, but are now called by three different names, even though they share an underlying implementation (which has a general air of being over-engineered, thanks to its once-mightier state).

enum NEPTUNE_MODE from 1
enum INDEXT_MODE
enum I6TCODE_MODE
void I6T::interpret_neptune(filename *neptune_file) {
    I6T::interpreter_shared(Task::syntax_tree(), NEPTUNE_MODE, NULL, NULL, NULL, -1, neptune_file);
}

int do_not_generate_index = FALSE;  Set by the -no-index command line option
void I6T::disable_or_enable_index(int which) {
    do_not_generate_index = which;
}

void I6T::interpret_indext(filename *indext_file) {
    if (do_not_generate_index == FALSE)
        I6T::interpreter_shared(Task::syntax_tree(), INDEXT_MODE, NULL, NULL, NULL, -1, indext_file);
}

void I6T::interpret_i6t(OUTPUT_STREAM, wchar_t *sf, int N_escape) {
    I6T::interpreter_shared(Task::syntax_tree(), I6TCODE_MODE, OUT, sf, NULL, N_escape, NULL);
}

§2. Implementation. So, then, here is the shared interpreter for these functions. Broadly speaking, it's a filter from input to output, where the input is either to be a file or a wide C-string, and the output (if any) is a text stream. In kind or indexing mode, there is in fact no output, and the interpreter is run only to call other functions.

void I6T::interpreter_shared(parse_node_tree *T, int int_mode, OUTPUT_STREAM, wchar_t *sf, text_stream *segment_name,
    int N_escape, filename *index_structure) {
    FILE *Input_File = NULL;
    int col = 1, cr, sfp = 0, lc = 0;
    TEMPORARY_TEXT(heading_name)

    int comment = FALSE;
    if ((int_mode == I6TCODE_MODE) && (Str::len(segment_name) > 0)) comment = TRUE;

    Open a file for input, if necessary2.1;

    TEMPORARY_TEXT(command)
    TEMPORARY_TEXT(argument)
    do {
        Str::clear(command);
        Str::clear(argument);
        Read next character from I6T stream2.2;
        NewCharacter: if (cr == EOF) break;
        if (comment == FALSE) {
            if (int_mode == NEPTUNE_MODE) {
                lc++;
                if ((cr == 10) || (cr == 13)) continue;  skip blank lines here
                Read rest of line as argument2.3;
                if ((Str::get_first_char(argument) == '!') ||
                    (Str::get_first_char(argument) == 0)) continue;  skip blanks and comments
                text_file_position tfp = TextFiles::at(index_structure, lc);
                parse_node *cs = current_sentence;
                current_sentence = NULL;
                NeptuneFiles::read_command(argument, &tfp);
                current_sentence = cs;
                continue;
            }
            if (cr == '{') {
                Read next character from I6T stream2.2;
                if (cr == '-') {
                    Read up to the next close brace as an I6T command and argument2.4;
                    if (Str::get_first_char(command) == '!') continue;
                    Act on I6T command and argument2.6;
                    continue;
                } else if ((cr == 'N') && (N_escape >= 0)) {
                    Read next character from I6T stream2.2;
                    if (cr == '}') {
                        WRITE("%d", N_escape);
                        continue;
                    }
                    if (OUT) WRITE("{N");
                    goto NewCharacter;
                } else {  otherwise the open brace was a literal
                    if (OUT) PUT_TO(OUT, '{');
                    goto NewCharacter;
                }
            }
            if (cr == '(') {
                Read next character from I6T stream2.2;
                if (cr == '+') {
                    Read up to the next plus close-bracket as an I7 expression2.5;
                    continue;
                } else {  otherwise the open bracket was a literal
                    if (OUT) PUT_TO(OUT, '(');
                    goto NewCharacter;
                }
            }
            if (OUT) PUT_TO(OUT, cr);
        }
    } while (cr != EOF);
    DISCARD_TEXT(command)
    DISCARD_TEXT(argument)
    if (Input_File) { if (DL) STREAM_FLUSH(DL); fclose(Input_File); }

    DISCARD_TEXT(heading_name)
}

§2.1. "If necessary" because our input may be supplied as a wide string, not a file.

Open a file for input, if necessary2.1 =

    if (Str::len(segment_name) > 0) {
        Input_File = NULL;
        WRITE_TO(STDERR, "inform: Unable to open segment <%S>\n", segment_name);
        StandardProblems::unlocated_problem(Task::syntax_tree(),
            _p_(BelievedImpossible),  or anyway not usefully testable
            "I couldn't open a requested I6T segment: see the console "
            "output for details.");
    } else if (index_structure) {
        Input_File = Filenames::fopen(index_structure, "r");
        if (Input_File == NULL) {
            LOG("Filename was %f\n", index_structure);
            if (int_mode == NEPTUNE_MODE)
            StandardProblems::unlocated_problem(Task::syntax_tree(),
                _p_(BelievedImpossible),  or anyway not usefully testable
                "I couldn't open a Neptune file for defining built-in kinds.");
            else
            StandardProblems::unlocated_problem(Task::syntax_tree(),
                _p_(BelievedImpossible),  or anyway not usefully testable
                "I couldn't open the template file for the index.");
        }
    }

§2.2. I6 template files are encoded as ISO Latin-1, not as Unicode UTF-8, so ordinary fgetc is used, and no BOM marker is parsed. Lines are assumed to be terminated with either 0x0a or 0x0d. (Since blank lines are harmless, we take no trouble over 0a0d or 0d0a combinations.) The built-in template files, almost always the only ones used, are line terminated 0x0a in Unix fashion.

Read next character from I6T stream2.2 =

    if (Input_File) cr = fgetc(Input_File);
    else if (sf) {
        cr = sf[sfp]; if (cr == 0) cr = EOF; else sfp++;
    } else cr = EOF;
    col++; if ((cr == 10) || (cr == 13)) col = 0;

§2.3. We get here when reading a kinds template file. Note that initial and trailing white space on the line is deleted: this makes it easier to lay out I6T template files tidily.

Read rest of line as argument2.3 =

    Str::clear(argument);
    if (Characters::is_space_or_tab(cr) == FALSE) PUT_TO(argument, cr);
    int at_start = TRUE;
    while (TRUE) {
        Read next character from I6T stream2.2;
        if ((cr == 10) || (cr == 13)) break;
        if ((at_start) && (Characters::is_space_or_tab(cr))) continue;
        PUT_TO(argument, cr); at_start = FALSE;
    }
    while (Characters::is_space_or_tab(Str::get_last_char(argument)))
        Str::delete_last_character(argument);

§2.4. And here we read a normal command. The command name must not include } or :. If there is no : then the argument is left unset (so that it will be the empty string: see above). The argument must not include }.

Read up to the next close brace as an I6T command and argument2.4 =

    Str::clear(command);
    Str::clear(argument);
    int com_mode = TRUE;
    while (TRUE) {
        Read next character from I6T stream2.2;
        if ((cr == '}') || (cr == EOF)) break;
        if ((cr == ':') && (com_mode)) { com_mode = FALSE; continue; }
        if (com_mode) PUT_TO(command, cr);
        else PUT_TO(argument, cr);
    }

§2.5. I7 expressions can be included in I6T code exactly as in inline invocation definitions: thus

    Constant FROG_CL = (+ pond-dwelling amphibian +);

will expand "pond-dwelling amphibian" into the I6 translation of the kind of object with this name. Because of this syntax, one has to watch out for I6 code like so:

    if (++counter_of_some_kind > 0) ...

which can trigger an unwanted (+.

Read up to the next plus close-bracket as an I7 expression2.5 =

    TEMPORARY_TEXT(i7_exp)
    while (TRUE) {
        Read next character from I6T stream2.2;
        if (cr == EOF) break;
        if ((cr == ')') && (Str::get_last_char(i7_exp) == '+')) {
            Str::delete_last_character(i7_exp); break; }
        PUT_TO(i7_exp, cr);
    }
    wording W = Feeds::feed_text(i7_exp);
    CSIInline::eval_bracket_plus_to_text(OUT, W);
    DISCARD_TEXT(i7_exp)

§2.6. Acting on I6T commands. At one time there were very many commands avalable here, but no longer.

Act on I6T command and argument2.6 =

    if (int_mode == INDEXT_MODE) Act on an I6T indexing command2.6.1;

    LOG("command: <%S> argument: <%S>\n", command, argument);
    Problems::quote_stream(1, command);
    StandardProblems::unlocated_problem(Task::syntax_tree(), _p_(PM_TemplateError),
        "In an explicit Inform 6 code insertion, I recognise a few special "
        "notations in the form '{-command}'. This time, though, the unknown notation "
        "{-%1} has been used, and this is an error. (It seems very unlikely indeed "
        "that this could be legal Inform 6 which I'm misreading, but if so, try "
        "adjusting the spacing to make this problem message go away.)");

§2.6.1. Indexing commands. Commands in a .indext file are skipped when Inform has been called with a ommand-line switch to disable the index. (As is done by intest, to save time.) {-index:name} opens the index file called name.

Act on an I6T indexing command2.6.1 =

    if (Str::eq_wide_string(command, L"index-complete")) { Index::complete(); continue; }

    if (Str::eq_wide_string(command, L"index-page")) {
        match_results mr = Regexp::create_mr();
        if (Regexp::match(&mr, argument, L"(%c+?)=(%c+?)=(%c+)")) {
            text_stream *col = mr.exp[0];
            text_stream *titling = mr.exp[1];
            text_stream *explanation = mr.exp[2];
            match_results mr2 = Regexp::create_mr();
            text_stream *leafname = titling;
            if (Regexp::match(&mr2, titling, L"(%C+?) (%c+)")) leafname = mr2.exp[0];
            Index::new_page(col, titling, explanation, leafname);
            Regexp::dispose_of(&mr2);
        } else internal_error("bad index-page format");
        Regexp::dispose_of(&mr);
        continue;
    }

    if (Str::eq_wide_string(command, L"index-element")) {
        match_results mr = Regexp::create_mr();
        if (Regexp::match(&mr, argument, L"(%C+) (%c+?)=(%c+)"))
            Index::new_segment(mr.exp[0], mr.exp[1], mr.exp[2]);
        else internal_error("bad index-element format");
        Regexp::dispose_of(&mr);
        continue;
    }

    if (Str::eq_wide_string(command, L"index")) {
        match_results mr = Regexp::create_mr();
        if (Regexp::match(&mr, argument, L"(%c+?)=(%c+)")) {
            text_stream *titling = mr.exp[0];
            text_stream *explanation = mr.exp[1];
            match_results mr2 = Regexp::create_mr();
            TEMPORARY_TEXT(leafname)
            Str::copy(leafname, titling);
            if (Regexp::match(&mr2, leafname, L"(%C+?) (%c+)")) Str::copy(leafname, mr2.exp[0]);
            WRITE_TO(leafname, ".html");
            Index::open_file(leafname, titling, -1, explanation);
            Regexp::dispose_of(&mr2);
            DISCARD_TEXT(leafname)
        } else {
            internal_error("bad index format");
        }
        Regexp::dispose_of(&mr);
        continue;
    }

§3. Indexing. And so, finally, the following triggers the indexing process.

void I6T::produce_index(void) {
    inform_project *project = Task::project();
    I6T::interpret_indext(
        Filenames::in(
            Languages::path_to_bundle(
                Projects::get_language_of_index(project)),
            Projects::index_structure(project)));
}