To assemble and format problem messages within the problem buffer.


§1. Problem messages begin with an indication of where in the source text the problem occurs, in terms of headings and subheadings written in the source text, and it is this indication that we consider first. For example,

In Part the First, Chapter 1 - Attic Area:

There can be up to 10 levels in the hierarchy of headings and subheadings, with level 0 the top level and level 9 the lowest.

When we need to issue a problem at sentence S, we work out what the current heading is (if any) at each of the 10 levels. We do this by trekking right through the whole linked list of sentences until we reach S, changing the current headings whenever we pass one. This sounds inefficient, but of course few problems are issued, and in any case we cannot optimise by simply cacheing the heading level from one problem to the next because it is not true that problems are always issued in source code order.

    void Problems::find_headings_at(parse_node_tree *T, parse_node *sentence, parse_node **problem_headings) {
        for (int i=0; i<NO_HEADING_LEVELS; i++) problem_headings[i] = NULL;
        if (sentence == NULL) return;
        ParseTree::traverse_ppn_nocs(T, Problems::visit_for_headings, &sentence);
        parse_node *p = ParseTree::get_problem_falls_under(sentence);
        while (p) {
            int L = ParseTree::int_annotation(p, heading_level_ANNOT);
            problem_headings[L] = p;
            p = ParseTree::get_problem_falls_under(p);
        }
    }

    int Problems::visit_for_headings(parse_node *p, parse_node *from, parse_node **sentence) {
        if (p == *sentence) {
            ParseTree::set_problem_falls_under(p, from);
            return TRUE;
        }
        if (ParseTree::get_type(p) == HEADING_NT) {
            ParseTree::set_problem_falls_under(p, from);
        }
        return FALSE;
    }

The function Problems::find_headings_at is used in §3.

The function Problems::visit_for_headings appears nowhere else.

§2. A further refinement is that we remember the last set of headings used for an error message, and only mention what has changed about the location. Thus we might next print:

In Chapter 2 - Cellar Area:

omitting to mention "Part the First" this time, since that part has not changed. (And we never print internally made level 0, File, headings.)

    parse_node *last_problem_headings[NO_HEADING_LEVELS];

§3. Now for the actual displaying of the location text. The outcome image is a trickier thing to get right than might appear. By being in this routine, we know that a problem has been issued: the run will therefore not have been a success, and we can issue the "NI failed" outcome image. A success image cannot be placed until right at the end of the run, when all possibility of problems has been passed: so there's no single point in NI where a single line of code could choose between the two possibilities.

    int	do_not_locate_problems = FALSE;

    void Problems::show_problem_location(parse_node_tree *T) {
        parse_node *problem_headings[NO_HEADING_LEVELS];
        int i, f = FALSE;
        if (problem_count == 0) {
            #ifdef CORE_MODULE
            if (internal_error_thrown)
                HTMLFiles::html_outcome_image(problems_file, "ni_failed_badly", "Failed");
            else
                HTMLFiles::html_outcome_image(problems_file, "ni_failed", "Failed");
            #endif
            for (i=0; i<NO_HEADING_LEVELS; i++) last_problem_headings[i] = NULL;
        }
        if (do_not_locate_problems) return;
        Problems::find_headings_at(T, current_sentence, problem_headings);
        for (i=0; i<NO_HEADING_LEVELS; i++) if (problem_headings[i] != NULL) f = TRUE;
        if (f)
            for (i=1; i<NO_HEADING_LEVELS; i++)
                if (last_problem_headings[i] != problem_headings[i]) {
                    <Print the heading position 3.1>;
                    break;
                }
        for (i=0; i<NO_HEADING_LEVELS; i++) last_problem_headings[i] = problem_headings[i];
    }

The function Problems::show_problem_location is used in §9.

§3.1. We print only the part of the heading position which differs from that of the previous one quoted: i is at this point the highest level at which they differ.

<Print the heading position 3.1> =

        source_file *pos = NULL;
        Problems::Buffer::clear();
        if (problem_count > 0) WRITE_TO(PBUFF, ">---> ");
        WRITE_TO(PBUFF, "In");
        for (f=FALSE; i<NO_HEADING_LEVELS; i++)
            if (problem_headings[i] != NULL) {
                wording W = ParseTree::get_text(problem_headings[i]);
                #ifdef INBUILD_MODULE
                W = Headings::get_text(Headings::from_node(problem_headings[i]));
                #endif
                pos = Lexer::file_of_origin(Wordings::first_wn(W));
                if (f) WRITE_TO(PBUFF, ", ");
                else WRITE_TO(PBUFF, " ");
                f = TRUE;
                Problems::Buffer::copy_text_into_problem_buffer(W);
            }
        if (f == FALSE) WRITE_TO(PBUFF, " the main source text");
        if (pos) {
            #ifdef INBUILD_MODULE
            inform_extension *E = Extensions::corresponding_to(pos);
            if (E) WRITE_TO(PBUFF, " in the extension %X", E->as_copy->edition->work);
            #endif
        }
        WRITE_TO(PBUFF, ":");
        Problems::Buffer::output_problem_buffer(0);
        Problems::Buffer::clear();

This code is used in §3.

§4. Problem quotations. Problem messages are printed using a printf-like formatting system. Unlike printf, though, where %s means a string and %d a number, here the escape codes do not indicate the type of the data: they are simply %1, %2, %3, ..., %9. This is to prevent horrendous crashes when type mismatches occur: using a pointer to a phrase when trying to print a source code reference, for instance.

The texts to be substituted in place of %1, %2, ..., are called the "quotations". The value is either a range of words in the source text, or else a pointer to some data structure, depending on the type. The type is a single character code. (This coding system is used only here, and could easily be changed, but there seems no reason to.)

The type codes are S(ource), W(ords), B(inary predicate), E(xtension), (p)H(rase), I(nvocation), N(umber), O(bject), P(roperty name), T(ext), (t)Y(pe specification).

    typedef struct problem_quotation {
        char quotation_type;     one of the above
        int wording_based;     which of the following:
        void *structure_quoted;     if false
        void (*expander)(text_stream *, void *);     if false
        struct wording text_quoted;     if true
    } problem_quotation;

    problem_quotation problem_quotations[10];

The structure problem_quotation is private to this section.

§5. When some higher-level part of NI wants to issue a formatted problem message, it first declares the contents of any quotations it will make. It does this using the routines Problems::quote_object, Problems::quote_spec, ... below. Thus Problems::quote_spec(2, SP) specifies that %2 should be printed as the inference SP.

    void Problems::problem_quote(int t, void *v, void (*f)(text_stream *, void *)) {
        if ((t<0) || (t > 10)) internal_error("problem quotation number out of range");
        problem_quotations[t].structure_quoted = v;
        problem_quotations[t].expander = f;
        problem_quotations[t].quotation_type = '?';
        problem_quotations[t].wording_based = FALSE;
        problem_quotations[t].text_quoted = EMPTY_WORDING;
    }

    void Problems::problem_quote_textual(int t, char type, wording W) {
        if ((t<0) || (t > 10)) internal_error("problem quotation number out of range");
        problem_quotations[t].structure_quoted = NULL;
        problem_quotations[t].quotation_type = type;
        problem_quotations[t].wording_based = TRUE;
        problem_quotations[t].text_quoted = W;
    }

The function Problems::problem_quote is used in §6.

The function Problems::problem_quote_textual is used in §6.

§6. Here are the three public routines for quoting from text: either via a node in the parse tree, or with a literal word range.

    void Problems::quote_source(int t, parse_node *p) {
        if (p == NULL) Problems::problem_quote_textual(t, 'S', EMPTY_WORDING);
        else Problems::quote_wording_as_source(t, ParseTree::get_text(p));
    }
    void Problems::quote_source_eliding_begin(int t, parse_node *p) {
        if (p == NULL) Problems::problem_quote_textual(t, 'S', EMPTY_WORDING);
        else {
            wording W = ParseTree::get_text(p);
            #ifdef CORE_MODULE
            if (<phrase-beginning-block>(W)) W = GET_RW(<phrase-beginning-block>, 1);
            #endif
            Problems::problem_quote_textual(t, 'S', W);
        }
    }
    void Problems::quote_wording(int t, wording W) { Problems::problem_quote_textual(t, 'W', W); }
    void Problems::quote_wording_tinted_green(int t, wording W) { Problems::problem_quote_textual(t, 'g', W); }
    void Problems::quote_wording_tinted_red(int t, wording W) { Problems::problem_quote_textual(t, 'r', W); }
    void Problems::quote_wording_as_source(int t, wording W) { Problems::problem_quote_textual(t, 'S', W); }

    void Problems::quote_text(int t, char *p) {
        Problems::problem_quote(t, (void *) p, Problems::expand_text);
    }
    void Problems::expand_text(OUTPUT_STREAM, void *p) {
        WRITE("%s", (char *) p);
    }
    void Problems::quote_wide_text(int t, wchar_t *p) {
        Problems::problem_quote(t, (void *) p, Problems::expand_wide_text);
    }
    void Problems::expand_wide_text(OUTPUT_STREAM, void *p) {
        WRITE("%w", (wchar_t *) p);
    }
    void Problems::quote_stream(int t, text_stream *p) {
        Problems::problem_quote(t, (void *) p, Problems::expand_stream);
    }
    void Problems::expand_stream(OUTPUT_STREAM, void *p) {
        WRITE("%S", (text_stream *) p);
    }
    void Problems::quote_wa(int t, word_assemblage *p) {
        Problems::problem_quote(t, (void *) p, Problems::expand_wa);
    }
    void Problems::expand_wa(OUTPUT_STREAM, void *p) {
        WRITE("%A", (word_assemblage *) p);
    }

    void Problems::expand_text_within_reason(OUTPUT_STREAM, wording W) {
        W = Wordings::truncate(W, QUOTATION_TOLERANCE_LIMIT);
        WRITE("%<W", W);
    }

    void Problems::quote_number(int t, int *p) {
        Problems::problem_quote(t, (void *) p, Problems::expand_number);
    }
    void Problems::expand_number(OUTPUT_STREAM, void *p) {
        WRITE("%d", *((int *) p));
    }

The function Problems::quote_source is used in 2/pl3 (§3, §13, §15, §16, §17, §19, §20).

The function Problems::quote_source_eliding_begin appears nowhere else.

The function Problems::quote_wording is used in 2/pl3 (§17, §19, §20).

The function Problems::quote_wording_tinted_green appears nowhere else.

The function Problems::quote_wording_tinted_red appears nowhere else.

The function Problems::quote_wording_as_source appears nowhere else.

The function Problems::quote_text is used in 2/pl3 (§3, §12, §13, §15, §16, §17, §19, §20).

The function Problems::expand_text appears nowhere else.

The function Problems::quote_wide_text is used in 2/pl3 (§13).

The function Problems::expand_wide_text appears nowhere else.

The function Problems::quote_stream is used in 2/pl3 (§3, §13, §14).

The function Problems::expand_stream appears nowhere else.

The function Problems::quote_wa appears nowhere else.

The function Problems::expand_wa appears nowhere else.

The function Problems::expand_text_within_reason appears nowhere else.

The function Problems::quote_number is used in 2/pl3 (§3, §12).

The function Problems::expand_number appears nowhere else.

§7. Short and long forms. Most of the error messages have a short form, giving the main factual information, and a long form which also has an explanation attached. Some of the text is common to both versions, but other parts are specific to either one or the other, and variables record the status of our current position in scanning and transcribing the problem message.

    int shorten_problem_message;     give short form of this previously-seen problem
    int reading_text_specific_to_short_version;     modes during problem message writing
    int reading_text_specific_to_long_version;

§8. We do not want to keep wittering on with the same old explanations, so we remember which ones we've given before (i.e., in previous problem messages during the same run of NI). Eventually our patience is exhausted and we give no further explanation in any circumstances.

    define PATIENCE_EXHAUSTION_POINT 100
    char *explanations[PATIENCE_EXHAUSTION_POINT];
    int no_explanations = 0;

    int Problems::explained_before(char *explanation) {
        int i;
        if (no_explanations == PATIENCE_EXHAUSTION_POINT) return TRUE;
        for (i=0; i<no_explanations; i++)
            if (explanation == explanations[i]) return TRUE;
        explanations[no_explanations++] = explanation;
        return FALSE;
    }

The function Problems::explained_before is used in §9.

§9. How problems begin and end. During the construction of a problem message, we will be running through a standard text, and at any point might be considering matter which should appear only in the long form, or only in the short form.

If the text of a message begins with an asterisk, then it is a continuation of a message already partly issued. Otherwise we can sensibly find out whether this is one we've seen before. Either way, we set shorten_problem_message to remember whether to use the short or long form.

    void Problems::issue_problem_begin(parse_node_tree *T, char *message) {
        Problems::Buffer::clear();
        if (strcmp(message, "*") == 0) {
            WRITE_TO(PBUFF, ">++>");
            shorten_problem_message = FALSE;
        } else if (strcmp(message, "****") == 0) {
            WRITE_TO(PBUFF, ">++++>");
            shorten_problem_message = FALSE;
        } else if (strcmp(message, "***") == 0) {
            WRITE_TO(PBUFF, ">+++>");
            shorten_problem_message = FALSE;
        } else if (strcmp(message, "**") == 0) {
            shorten_problem_message = FALSE;
        } else {
            if (T) Problems::show_problem_location(T);
            problem_count++;
            WRITE_TO(PBUFF, ">--> ");
            shorten_problem_message = Problems::explained_before(message);
        }
        reading_text_specific_to_short_version = FALSE;
        reading_text_specific_to_long_version = FALSE;
    }

    void Problems::issue_problem_end(void) {
        #ifdef CORE_MODULE
        if (compiling_text_routines_mode) Strings::TextSubstitutions::append_text_substitution_proviso();
        #endif
        Problems::Buffer::output_problem_buffer(1);
        Problems::Issue::problem_documentation_links(problems_file);
        if (crash_on_all_errors) Problems::Fatal::force_crash();
    }

The function Problems::issue_problem_begin is used in 2/pl3 (§3, §11, §12, §13, §14, §15, §16, §17, §19, §20).

The function Problems::issue_problem_end is used in 2/pl3 (§2, §12, §13, §14, §15, §16, §17, §19, §20).

§10. Appending source.

    wording appended_source = EMPTY_WORDING;
    void Problems::append_source(wording W) {
        appended_source = W;
    }
    void Problems::transcribe_appended_source(void) {
        if (Wordings::nonempty(appended_source))
            Problems::Buffer::copy_source_reference_into_problem_buffer(appended_source);
    }

The function Problems::append_source is used in §11.1.2.

The function Problems::transcribe_appended_source is used in §11.1.2.

§11. Issuing a segment of a problem message. Here is the routine which performs the substitution of quotations into problem messages and sends them on their way: which is called Problems::issue_problem_segment since it only appends a further piece of text, and may be used several times to build up complicated messages.

We have seen that, within problem message texts, the escapes %1 to %9 are to produce quotations. Four further escape codes switch between problem message versions, as follows: we have %L, which indicates that the text which follows should only be used in the long form of the error message; %S, the same for the short form; %%, which cancels either of these settings and restores the text to appearing in both forms, which is the default. A percentage sign followed by a vertical stroke acts more simply as a divider between the brief message and the explanation text in many simple problem messages which do not use the elaborations of the other three escapes.

    void Problems::issue_problem_segment(char *message) {
        for (int i=0; message[i]; i++) {
            if (message[i] == '%') {
                switch (message[i+1]) {
                    case 'L': reading_text_specific_to_long_version = TRUE; i++; continue;
                    case 'S': reading_text_specific_to_short_version = TRUE; i++; continue;
                    case '%': reading_text_specific_to_short_version = FALSE;
                        reading_text_specific_to_long_version = FALSE; i++; continue;
                    case '|': reading_text_specific_to_short_version = FALSE;
                        reading_text_specific_to_long_version = TRUE;
                        if (shorten_problem_message) WRITE_TO(PBUFF, ".");
                        i++; continue;
                }
            }

            if ((reading_text_specific_to_short_version) &&
                (shorten_problem_message == FALSE)) continue;
            if ((reading_text_specific_to_long_version) &&
                (shorten_problem_message == TRUE)) continue;

            <Act on the problem message text, since it is now contextually allowed 11.1>;
        }
    }

The function Problems::issue_problem_segment is used in 2/pl3 (§3, §12, §13, §14, §15, §16, §17, §19, §20).

§11.1. Ordinarily we just append the new character, but we also act on the escapes %P and %1 to %9. %P forces a paragraph break, or at any rate, it does in the eventual HTML version of the problem message. Note that these escapes are acted on only if they occur in a contextually allowed part of the problem message (e.g., if they occur in the short form only, they will only be acted on when the shortened form is the one being issued).

<Act on the problem message text, since it is now contextually allowed 11.1> =

        if (message[i] == '%') {
            switch (message[i+1]) {
                case 'P': PUT_TO(PBUFF, FORCE_NEW_PARA_CHAR);
                    i++; continue;
            }
            if (Characters::isdigit(message[i+1])) {
                int t = ((int) (message[i+1]))-((int) '0'); i++;
                if ((t>=1) && (t<=9)) {
                    if (problem_quotations[t].wording_based)
                        <Expand wording-based escape 11.1.1>
                    else
                        <Expand structure-based escape 11.1.2>
                }
                continue;
            }
        }
        PUT_TO(PBUFF, message[i]);

This code is used in §11.

§11.1.1. This is where a quotation escape, such as %2, is expanded: by looking up its type, stored internally as a single character.

<Expand wording-based escape 11.1.1> =

        switch(problem_quotations[t].quotation_type) {
            Monochrome wording
            case 'S': Problems::Buffer::copy_source_reference_into_problem_buffer(
                    problem_quotations[t].text_quoted);
                break;
            case 'W': Problems::Buffer::copy_text_into_problem_buffer(
                    problem_quotations[t].text_quoted);
                break;

            Tinted wording
            case 'r': <Quote a red-tinted word range in a problem message 11.1.1.1>;
                break;
            case 'g': <Quote a green-tinted word range in a problem message 11.1.1.2>;
                break;
            default: internal_error("unknown error token type");
        }

This code is used in §11.1.

§11.1.1.1. Tinting text involves some HTML, of course:

<Quote a red-tinted word range in a problem message 11.1.1.1> =

        TEMPORARY_TEXT(OUT);
        HTML::begin_colour(OUT, I"800000");
        WRITE("%W", problem_quotations[t].text_quoted);
        HTML::end_colour(OUT);
        <Spool temporary stream text to the problem buffer 11.1.1.1.1>;
        DISCARD_TEXT(OUT);

This code is used in §11.1.1.

§11.1.1.2. And:

<Quote a green-tinted word range in a problem message 11.1.1.2> =

        TEMPORARY_TEXT(OUT);
        HTML::begin_colour(OUT, I"008000");
        WRITE("%W", problem_quotations[t].text_quoted);
        HTML::end_colour(OUT);
        <Spool temporary stream text to the problem buffer 11.1.1.1.1>;
        DISCARD_TEXT(OUT);

This code is used in §11.1.1.

§11.1.2. More generally, the reference is to some structure we can't write ourselves, and must delegate to:

<Expand structure-based escape 11.1.2> =

        Problems::append_source(EMPTY_WORDING);
        TEMPORARY_TEXT(OUT);
        (problem_quotations[t].expander)(OUT, problem_quotations[t].structure_quoted);
        <Spool temporary stream text to the problem buffer 11.1.1.1.1>;
        DISCARD_TEXT(OUT);
        Problems::transcribe_appended_source();

This code is used in §11.1.

§11.1.1.1.1. <Spool temporary stream text to the problem buffer 11.1.1.1.1> =

        LOOP_THROUGH_TEXT(pos, OUT) {
            wchar_t c = Str::get(pos);
            if (c == '<') c = PROTECTED_LT_CHAR;
            if (c == '>') c = PROTECTED_GT_CHAR;
            PUT_TO(PBUFF, c);
        }

This code is used in §11.1.1.1, §11.1.1.2, §11.1.2.

§12. This version is much shorter, since escapes aren't allowed:

    void Problems::issue_problem_segment_from_stream(text_stream *message) {
        WRITE_TO(PBUFF, "%S", message);
    }

The function Problems::issue_problem_segment_from_stream appears nowhere else.

§13. Problems report and index. That gives us enough infrastructure to produce the final report. Note the use of error redirection to in order to put pseudo-problem messages — actually informational — into the report. In the case where the run was successful and there we no Problem messages, we have to be careful to reset problem_count — it will have been increased by the issuing of these pseudo-problems, and we need it to remain 0 so that main() can finally return back to the operating system without an error code.

    int tail_of_report_written = FALSE;
    void Problems::write_reports(int disaster_struck) {
        if (tail_of_report_written) return;
        tail_of_report_written = TRUE;

        crash_on_all_errors = FALSE;
        #ifdef PROBLEMS_FINAL_REPORTER
        int pc = problem_count;
        PROBLEMS_FINAL_REPORTER(disaster_struck, problem_count);
        problem_count = pc;
        #endif
        HTML::end_body(problems_file);
    }

The function Problems::write_reports is used in 2/pl3 (§2, §12).