The part of Inform most nearly like a typechecker in a conventional compiler.


§1. Dash is the second typechecking algorithm to be used in Inform, installed in early 2015: the first had served since 2003, but became unwieldy after so many exceptional cases had been added to it, and was impossible to adapt to the redesigned parse tree. Dash is not so called because it's faster (it's actually a few percent slower), but because at one stage Inform was running both typecheckers side by side: TC and TC-dash, or Dash for short. TC-dash won, it's still called Dash, and TC is no more.

Because Dash also deals with text which entirely fails to make sense, which in other compilers would be rejected at a lower level, it has to issue basic syntax errors as well as type mismatch errors. This is arguably a good thing, though, because it means they can be issued using the same generally helpful system as more sophisticated problems.

Partly because of the need to do this, the type-checker has a top-down approach. It aims to prove that the node found can match what's expected, making selections from alternative readings, and in limited cases actually making changes to the parse tree, in order to do this. For instance, consider checking the tree for:

let the score be the score plus 10

Dash takes the view that the phrase usage can be proved correct, so long as the arguments can also be proved. There are several valid interpretations of "let ... be ...", and these are all present in the parse tree as alternative interpretations, so the typechecker tries each in turn, accepting one (or more) if the arguments can be proved to be of the right type. This means proving that argument 0 ("the score") is an lvalue and also that argument 1 ("the score plus 10") is an rvalue. A further rule requires that the kind of value of argument 1 must match the kind of value stored in the variable, here a "number", so we must prove that too. Now "plus" is polymorphic and can produce different kinds of value depending on the kinds of value it acts upon, so again we must check all possible interpretations. But we finally succeed in showing that "score" is an lvalue, "10" is a number, "score" is also a number, and that "plus" on two numbers gives a number, so we complete the proof and the phrase is proved correct.

§2. When issuing problems, we show a form of backtrace so that the user can see what we've considered, and this is used to accumulate data for that.

typedef struct inv_token_problem_token {
    struct wording problematic_text;
    struct parse_node *as_parsed;
    int already_described;
    int new_name;  found in context of a name not yet defined
    CLASS_DEFINITION
} inv_token_problem_token;

§3. The Dashboard. Dash uses a small suite of global variables to keep track of two decidedly global side-effects of checking: the issuing of problem messages, and the setting of kind variables. This suite is called the "dashboard".

First, we keep track of the problem messages we will issue, if any, using a bitmap made up of the following modes:

define BEGIN_DASH_MODE         int s_dm = dash_mode;
                            kind **s_kvc = kind_of_var_to_create;
                            parse_node *s_invl = Dash_ambiguity_list;
define DASH_MODE_ENTER(mode)   dash_mode |= mode;
define DASH_MODE_CREATE(K)     kind_of_var_to_create = K;
define DASH_MODE_EXIT(mode)        dash_mode &= (~mode);
define END_DASH_MODE           dash_mode = s_dm;
                            kind_of_var_to_create = s_kvc;
                            Dash_ambiguity_list = s_invl;
define TEST_DASH_MODE(mode)        (dash_mode & mode)
define ISSUE_PROBLEMS_DMODE            0x00000001  rather than keep silent about them
define ISSUE_LOCAL_PROBLEMS_DMODE      0x00000002  at the end, that is
define ISSUE_GROSS_PROBLEMS_DMODE      0x00000004  at the end, that is
define ISSUE_INTERESTING_PROBLEMS_DMODE    0x00000008  unless casting to text
define ABSOLUTE_SILENCE_DMODE          0x00000010  say nothing at all
int dash_mode = ISSUE_PROBLEMS_DMODE;  default
kind **kind_of_var_to_create = NULL;
int dash_recursion_count = 0;

§4. Three grades of problem can appear: "ordinary", "gross" and "grosser than gross". We distinguish these in order to produce a Problem message which reflects the biggest thing wrong, rather than being so esoteric that it misses the main point. Changing a particular error condition from an ordinary to a gross problem, or vice versa, has no effect on the result returned by Dash, only on the Problem messages given to the user.

define THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM
    no_gross_problems_thrown++;  problems this gross cannot be suppressed
define THIS_IS_A_GROSS_PROBLEM
    no_gross_problems_thrown++;  this increments even if the message is suppressed
    if ((TEST_DASH_MODE(ISSUE_PROBLEMS_DMODE) == FALSE) &&
        (TEST_DASH_MODE(ISSUE_GROSS_PROBLEMS_DMODE) == FALSE)) return NEVER_MATCH;
define THIS_IS_AN_ORDINARY_PROBLEM
    if (TEST_DASH_MODE(ISSUE_PROBLEMS_DMODE) == FALSE) return NEVER_MATCH;
int no_gross_problems_thrown = 0;
int no_interesting_problems_thrown = 0;
int initial_problem_count = 0;
int backtraced_problem_count = 0;

int Dash::problems_have_been_issued(void) {
    if (initial_problem_count < problem_count) return TRUE;
    return FALSE;
}

§5. Next, we keep track of the most recent set of meanings attached to the kind variables A, B, C, ..., Z, and the most recently looked-at list of invocations.

kind_variable_declaration *most_recent_interpretation = NULL;
parse_node *Dash_ambiguity_list = NULL;

§6. We need careful debug logging of what Dash does. During Inform's infancy, the type checker was the hardest thing to debug, but that wasn't so much because this was the great habitat and breeding ground for bugs; it was more that those bugs which were here were by far the hardest to root out. So careful logging on demand is vital.

Each call to the recursive Dash has its own unique ID number, to make logging more legible.

define LOG_DASH_LEFT
    LOGIF(MATCHING, "[%d%s] ",
        unique_DR_call_identifier,
        (TEST_DASH_MODE(ISSUE_PROBLEMS_DMODE))?"":"-silent");
define LOG_DASH(stage)
    LOGIF(MATCHING, "[%d%s] %s $P\n",
        unique_DR_call_identifier,
        (TEST_DASH_MODE(ISSUE_PROBLEMS_DMODE))?"":"-silent", stage, p);
int unique_DR_call_identifier = 0, DR_call_counter = 0;  solely to make the log more legible

§7. Return values. Dash records the outcome of checking as one of three states.

It is perhaps telling that we never need a Dash::best_case routine. Typecheckers are not allowed to be optimistic.

int Dash::worst_case(int rv1, int rv2) {
    if ((rv1 == NEVER_MATCH) || (rv2 == NEVER_MATCH)) return NEVER_MATCH;
    if ((rv1 == SOMETIMES_MATCH) || (rv2 == SOMETIMES_MATCH)) return SOMETIMES_MATCH;
    return ALWAYS_MATCH;
}

§8. (1) Entering Dash. Dash is structured into levels and this is level 1, the topmost.

Dash has three points of entry: to check a condition, check a value, or check an invocation list for a phrase used in a routine.

These top-level routines do not look recursive, but in fact some can be, because Dash needs to call the predicate calculus engine to typecheck propositions: and these in turn call Dash to check that constant values are used correctly.

All of these funnel downwards into level 2:

int Dash::check_condition(parse_node *p) {
    parse_node *cn = Node::new(CONDITION_CONTEXT_NT);
    cn->down = p;
    LOGIF(MATCHING, "Dash (1): condition\n");
    return Dash::funnel_to_level_2(cn, FALSE);
}

int Dash::check_value(parse_node *p, kind *K) {
    parse_node *vn = Node::new(RVALUE_CONTEXT_NT);
    if (K) Node::set_kind_required_by_context(vn, K);
    vn->down = p;
    if (K) LOGIF(MATCHING, "Dash (1): value of kind %u\n", K);
    if (K == NULL) LOGIF(MATCHING, "Dash (1): value\n");
    return Dash::funnel_to_level_2(vn, FALSE);
}

int Dash::check_value_silently(parse_node *p, kind *K) {
    parse_node *vn = Node::new(RVALUE_CONTEXT_NT);
    if (K) Node::set_kind_required_by_context(vn, K);
    vn->down = p;
    if (K) LOGIF(MATCHING, "Dash (1): value of kind %u\n", K);
    if (K == NULL) LOGIF(MATCHING, "Dash (1): value\n");
    return Dash::funnel_to_level_2(vn, TRUE);
}

int Dash::check_invl(parse_node *p) {
    LOGIF(MATCHING, "Dash (1): invocation list '%W'\n", Node::get_text(p));
    LOGIF(MATCHING, "p = $T\n", p);
    return Dash::funnel_to_level_2(p, FALSE);
}

int Dash::funnel_to_level_2(parse_node *p, int silently) {
    no_gross_problems_thrown = 0;
    dash_recursion_count = 0;
    BEGIN_DASH_MODE;
    if (!silently) DASH_MODE_ENTER(ISSUE_PROBLEMS_DMODE);
    initial_problem_count = problem_count;
    DASH_MODE_CREATE(NULL);
    Latticework::show_frame_variables();
    int rv = Dash::typecheck_recursive(p, NULL, TRUE);
    END_DASH_MODE;
    return rv;
}

§9. (2) Recursion point. Loosely speaking, Dash works by visiting every node in the parse tree being examined with the following routine, which is therefore recursive as Dash heads ever downward.

The routine itself is really just an outer shell, though, and has two functions: it keeps the debugging log tidy (see above) and it produces the backtrace if the inner routine should throw a problem message.

The recursion limit below is clearly arbitrary, but is there to prevent the algorithm from slowing Inform unacceptably in the event of something like

say g + g + g + g + g + g + g + g + g + g + g + g + g + g + g + g + g + g + g + g + g + g + g;

where "g" is a term Inform doesn't recognise, because otherwise this will recurse through every possible interpretation of the plus sign (i.e. every possible order of operations).

define MAX_DASH_RECURSION 10000
int Dash::typecheck_recursive(parse_node *p, parse_node *context, int consider_alternatives) {
    if (p == NULL) internal_error("Dash on null node");

    if (dash_recursion_count >= MAX_DASH_RECURSION) return NEVER_MATCH;
    dash_recursion_count++;

    int outer_id = unique_DR_call_identifier;
    int problem_count_before = problem_count;
    unique_DR_call_identifier = DR_call_counter++;
    LOG_INDENT;
    LOG_DASH("(2)");
    int return_value = Dash::typecheck_recursive_inner(p, context, consider_alternatives);

    switch(return_value) {
        case ALWAYS_MATCH:    LOG_DASH_LEFT; LOGIF(MATCHING, "== always\n"); break;
        case SOMETIMES_MATCH: LOG_DASH_LEFT; LOGIF(MATCHING, "== sometimes\n"); break;
        case NEVER_MATCH:     LOG_DASH_LEFT; LOGIF(MATCHING, "== never\n"); break;
        default: internal_error("impossible verdict from Dash");
    }
    LOG_OUTDENT;

    if ((problem_count > problem_count_before) && (consider_alternatives))
        Consider adding a backtrace of what the type-checker was up to9.1;

    unique_DR_call_identifier = outer_id;
    return return_value;
}

§9.1. The backtrace is added to problem messages only if we have just been checking a phrase, and if it produced problems not previously seen. The trick here is to ensure that if we have

let X be a random wibble bibble spong;

then it will be the "random ..." phrase which is backtraced, and not the "let ..." phrase, even though that also goes wrong in turn.

Consider adding a backtrace of what the type-checker was up to9.1 =

    if (problem_count > backtraced_problem_count) {
        if ((p) && (p->down) &&
            (Node::get_type(p) == INVOCATION_LIST_NT)) {
            TextSubstitutions::it_is_not_worth_adding();
            Backtrace what phrase definitions the type-checker was looking at9.1.1;
            TextSubstitutions::it_is_worth_adding();
            backtraced_problem_count = problem_count;
        }
    }

§9.1.1. We skip proven invocations, and those never needed because of them, since those aren't in dispute; and we also skip groups not even reached, since they aren't where the problem lies. (This can happen when checking a compound "say", from a text substitution.)

Backtrace what phrase definitions the type-checker was looking at9.1.1 =

    parse_node *inv;
    LOOP_THROUGH_ALTERNATIVES(inv, p->down) LOG("$e\n", inv);

    int to_show = 0;
    LOOP_THROUGH_ALTERNATIVES(inv, p->down) {
        id_body *idb = Node::get_phrase_invoked(inv);
        if (IDTypeData::is_a_spare_say_X_phrase(&(idb->type_data))) continue;
        to_show++;
    }

    int announce = TRUE;
    text_stream *latest = Problems::latest_sigil();
    if (Str::eq_wide_string(latest, U"PM_AllInvsFailed")) announce = FALSE;

    if (announce) Produce the I was trying... banner9.1.1.1;
    Produce the list of possibilities9.1.1.2;
    int real_found = FALSE;
    Produce the tokens which were recognisable as something9.1.1.3;
    Produce the tokens which weren't recognisable as something9.1.1.5;
    Produce the tokens which were intentionally not recognisable as something9.1.1.4;
    if (real_found) Produce a note about real versus integer9.1.1.6;

§9.1.1.1. Produce the I was trying... banner9.1.1.1 =

    Problems::issue_problem_begin(Task::syntax_tree(), "*");
    if (to_show > 1)
        Problems::issue_problem_segment("I was trying to match one of these phrases:");
    else
        Problems::issue_problem_segment("I was trying to match this phrase:");
    Problems::issue_problem_end();

§9.1.1.2. Produce the list of possibilities9.1.1.2 =

    int shown = 0;
    LOOP_THROUGH_ALTERNATIVES(inv, p->down) {
        id_body *idb = Node::get_phrase_invoked(inv);
        if (IDTypeData::is_a_spare_say_X_phrase(&(idb->type_data))) continue;
        shown++;
        Problems::quote_number(1, &shown);
        Problems::quote_invocation(2, inv);
        if (announce == FALSE) {
            Problems::issue_problem_begin(Task::syntax_tree(), "***");
            announce = TRUE;
        } else {
            Problems::issue_problem_begin(Task::syntax_tree(), "****");
        }
        if (to_show > 1) Problems::issue_problem_segment("%1. %2");
        else Problems::issue_problem_segment("%2");
        Problems::issue_problem_end();
    }

§9.1.1.3. Produce the tokens which were recognisable as something9.1.1.3 =

    int any = FALSE;
    inv_token_problem_token *itpt;
    LOOP_OVER(itpt, inv_token_problem_token)
        if (Node::is(itpt->as_parsed, UNKNOWN_NT) == FALSE)
            if (itpt->already_described == FALSE) {
                itpt->already_described = TRUE;
                if (any == FALSE) {
                    any = TRUE;
                    Problems::issue_problem_begin(Task::syntax_tree(), "*");
                    Problems::issue_problem_segment("I recognised:");
                    Problems::issue_problem_end();
                }
                Produce this token9.1.1.3.1;
            }

§9.1.1.3.1. Produce this token9.1.1.3.1 =

    Problems::quote_wording_tinted_green(1, itpt->problematic_text);
    Problems::quote_spec(2, itpt->as_parsed);
    Problems::issue_problem_begin(Task::syntax_tree(), "****");
    if (Specifications::is_value(itpt->as_parsed)) {
        kind *K = Specifications::to_kind(itpt->as_parsed);
        int changed = FALSE;
        K = Kinds::substitute(K, NULL, &changed, FALSE);
        Problems::quote_kind(3, K);
        if (Kinds::eq(K, K_real_number)) real_found = TRUE;
        if (Lvalues::is_lvalue(itpt->as_parsed))
            Produce the token for an lvalue9.1.1.3.1.1
        else if (Node::is(itpt->as_parsed, PHRASE_TO_DECIDE_VALUE_NT))
            Produce the token for a phrase deciding a value9.1.1.3.1.2
        else
            Produce the token for a constant rvalue9.1.1.3.1.3;
    } else Problems::issue_problem_segment("%1 = %2");
    Problems::issue_problem_end();

§9.1.1.3.1.1. Produce the token for an lvalue9.1.1.3.1.1 =

    Problems::issue_problem_segment("%1 = %2, holding %3");

§9.1.1.3.1.2. Produce the token for a phrase deciding a value9.1.1.3.1.2 =

    char *seg = "%1 = an instruction to work out %3";
    if (K == NULL) seg = "%1 = a phrase";
    parse_node *found_invl = itpt->as_parsed->down;
    parse_node *inv;
    LOOP_THROUGH_ALTERNATIVES(inv, found_invl) {
        LOG("$e\n", inv);
        if (Dash::reading_passed(inv) == FALSE) {
            seg = "%1 = an instruction I think should work out %3, "
                "but which I can't make sense of";
            for (int i=0; i<Invocations::get_no_tokens(inv); i++) {
                parse_node *tok = Invocations::get_token_as_parsed(inv, i);
                if (Node::is(tok, UNKNOWN_NT)) {
                    Problems::quote_wording(4, Node::get_text(tok));
                    seg = "%1 = an instruction I think should work out %3, "
                        "but which I can't perform because '%4' doesn't make sense here";
                    break;
                }
            }
        }
    }
    Problems::issue_problem_segment(seg);

§9.1.1.3.1.3. Produce the token for a constant rvalue9.1.1.3.1.3 =

    char *seg = "%1 = %3";
    if (Rvalues::is_CONSTANT_construction(itpt->as_parsed, CON_property)) {
        property *prn = Node::get_constant_property(itpt->as_parsed);
        if (Properties::is_value_property(prn)) {
            binary_predicate *bp = ValueProperties::get_stored_relation(prn);
            if (bp) {
                seg = "%1 = %3, which is used to store %4, "
                    "but is not the same thing as the relation itself";
                Problems::quote_relation(4, bp);
            }
        }
    }
    Problems::issue_problem_segment(seg);

§9.1.1.4. Produce the tokens which were intentionally not recognisable as something9.1.1.4 =

    int unknowns = 0;
    inv_token_problem_token *itpt;
    LOOP_OVER(itpt, inv_token_problem_token)
        if ((Node::is(itpt->as_parsed, UNKNOWN_NT)) && (itpt->new_name))
            if (itpt->already_described == FALSE) {
                itpt->already_described = TRUE;
                if (unknowns < 5) {
                    Problems::quote_wording_tinted_red(++unknowns,
                        itpt->problematic_text);
                }
            }
    if (unknowns > 0) {
        Problems::issue_problem_begin(Task::syntax_tree(), "*");
        char *chunk = "";
        switch (unknowns) {
            case 1: chunk = "The name '%1' doesn't yet exist."; break;
            case 2: chunk = "The names '%1' and '%2' don't yet exist."; break;
            case 3: chunk = "The names '%1', '%2' and '%3' don't yet exist."; break;
            case 4: chunk = "The names '%1', '%2', '%3' and '%4' don't yet exist."; break;
            default: chunk = "The names '%1', '%2', '%3', '%4', and so on, don't yet exist."; break;
        }
        Problems::issue_problem_segment(chunk);
        Problems::issue_problem_end();
    }

§9.1.1.5. Produce the tokens which weren't recognisable as something9.1.1.5 =

    int unknowns = 0;
    inv_token_problem_token *itpt;
    LOOP_OVER(itpt, inv_token_problem_token)
        if ((Node::is(itpt->as_parsed, UNKNOWN_NT)) &&
            (itpt->new_name == FALSE))
            if (itpt->already_described == FALSE) {
                itpt->already_described = TRUE;
                if (unknowns < 5) {
                    Problems::quote_wording_tinted_red(++unknowns,
                        itpt->problematic_text);
                }
            }
    if (unknowns > 0) {
        Problems::issue_problem_begin(Task::syntax_tree(), "*");
        char *chunk = "";
        switch (unknowns) {
            case 1: chunk = "But I didn't recognise '%1'."; break;
            case 2: chunk = "But I didn't recognise '%1' or '%2'."; break;
            case 3: chunk = "But I didn't recognise '%1', '%2' or '%3'."; break;
            case 4: chunk = "But I didn't recognise '%1', '%2', '%3' or '%4'."; break;
            default: chunk = "But I didn't recognise '%1', '%2', '%3', '%4' and so on."; break;
        }
        Problems::issue_problem_segment(chunk);
        Problems::issue_problem_end();
    }

§9.1.1.6. Produce a note about real versus integer9.1.1.6 =

    Problems::issue_problem_begin(Task::syntax_tree(), "*");
    Problems::issue_problem_segment(
        " %PNote that Inform's kinds 'number' and 'real number' are not "
        "interchangeable. A 'number' like 7 can be used where a 'real "
        "number' is expected - it becomes 7.000 - but not vice versa. "
        "Use 'R to the nearest whole number' if you want to make a "
        "conversion.");
    Problems::issue_problem_end();

§10. (3) Context switching. After those epic preliminaries, we finally do some typechecking.

The scheme here is that our expectations of p depend on the context, and this is defined by some node higher in the current subtree than p, which we will call context. Most of the time this is the parent of p, but sometimes the grandparent or great-grandparent; and at the start of the recursion, when no context has appeared yet, it will be null. In effect, then, the tree we're checking contains its own instructions on how it should be checked. For example, the subtree

    CONDITION_CONTEXT_NT
        p

tells us that when we reach p it should be checked as a condition.

int Dash::typecheck_recursive_inner(parse_node *p, parse_node *context, int consider_alternatives) {
    LOG_DASH("(3)");
    switch (p->node_type) {
        case CONDITION_CONTEXT_NT:          Switch context10.1;

        case RVALUE_CONTEXT_NT:             Switch context10.1;
        case MATCHING_RVALUE_CONTEXT_NT:    Switch context to an rvalue matching a description10.5;
        case SPECIFIC_RVALUE_CONTEXT_NT:    Switch context to an rvalue matching a value10.6;
        case VOID_CONTEXT_NT:               Switch to a void context10.7;

        case LVALUE_CONTEXT_NT:             Switch context to an lvalue10.2;
        case LVALUE_TR_CONTEXT_NT:          Switch context to a table reference lvalue10.3;
        case LVALUE_LOCAL_CONTEXT_NT:       Switch context to an existing local variable lvalue10.4;

        case NEW_LOCAL_CONTEXT_NT:          Deal with a new local variable name10.8;

        default:                            Typecheck within current context10.9;
    }
    return NEVER_MATCH;  to prevent compiler warnings: unreachable in fact
}

§10.1. When we find a node like CONDITION_CONTEXT_NT, that becomes the new context and we move down to its only child.

define SWITCH_CONTEXT_AND_RECURSE(p) Dash::typecheck_recursive(p->down, p, TRUE)

Switch context10.1 =

    return SWITCH_CONTEXT_AND_RECURSE(p);

§10.2. Other context switches are essentially the same thing, plus a check that the value meets some extra requirement. For example:

Switch context to an lvalue10.2 =

    int rv = SWITCH_CONTEXT_AND_RECURSE(p);
    if (Lvalues::is_lvalue(p->down) == FALSE)
        Issue problem for not being an lvalue10.2.1;
    return rv;

§10.3. More specifically:

Switch context to a table reference lvalue10.3 =

    int rv = SWITCH_CONTEXT_AND_RECURSE(p);
    if (Node::is(p->down, TABLE_ENTRY_NT) == FALSE)
        Issue problem for not being a table reference10.3.1;
    return rv;

§10.4. Switch context to an existing local variable lvalue10.4 =

    int rv = SWITCH_CONTEXT_AND_RECURSE(p);
    if (Node::is(p->down, LOCAL_VARIABLE_NT) == FALSE)
        Issue problem for not being an existing local10.4.1;
    return rv;

§10.5. Suppose we are matching the parameter of a phrase like this:

To inspect (D - an open door): ...

and typechecking the following invocation:

inspect the Marble Portal;

Then we would have p set to some value — here "the Marble Portal" — and the MATCHING_RVALUE_CONTEXT_NT node would point to a description node for open doors. We must see if p matches that. Any match can be at best at the "sometimes" level. We can prove the Marble Portal is a door at compile time, but we can't prove it's open until run-time.

Note that we switch context and recurse first, then make the supplementary check afterwards, when we know the kinds at least must be right.

Switch context to an rvalue matching a description10.5 =

    int rv = SWITCH_CONTEXT_AND_RECURSE(p);
    if (rv != NEVER_MATCH)
        rv = Dash::worst_case(rv,
            Dash::compatible_with_description(p->down,
                Node::get_token_to_be_parsed_against(p)));
    return rv;

§10.6. This is something else that wouldn't appear in a typical typechecker. Here we are dealing with a phrase specification such as:

To attract (N - 10) things: ...

where the "N" argument will be accepted if and only if it's the value 10. The fact that Inform allows this is further evidence of the slippery way that natural language doesn't distinguish values from types; early designs of Inform didn't allow it, but many people reported this as a bug.

Again we switch context and recurse first. We can't safely test pointer values, such as texts, for equality at compile time — for one thing, we don't know what text substitutions will then expand to — so the value test only forces us towards never or always when the constants being compared are word values.

Switch context to an rvalue matching a value10.6 =

    int rv = SWITCH_CONTEXT_AND_RECURSE(p);
    if (rv != NEVER_MATCH) {
        kind *K = Specifications::to_kind(p->down);
        if ((Kinds::Behaviour::uses_block_values(K) == FALSE) &&
            (Node::is(p->down, CONSTANT_NT))) {
            parse_node *val = Node::get_token_to_be_parsed_against(p);
            if (!(Rvalues::compare_CONSTANT(p->down, val)))
                Issue problem for being the wrong rvalue10.6.1;
        } else {
            rv = Dash::worst_case(rv, SOMETIMES_MATCH);
            LOGIF(MATCHING, "dropping to sometimes level for value comparison\n");
        }
    }
    return rv;

§10.7. I would ideally like to remove void contexts from Dash entirely, but was forced to retain them by the popularity of the Hypothetical Questions extension, which made use of the old undocumented phrase token.

Switch to a void context10.7 =

    int rv = SWITCH_CONTEXT_AND_RECURSE(p);
    if (rv != NEVER_MATCH) {
        if (!(Node::is(p->down, PHRASE_TO_DECIDE_VALUE_NT))) {
            Issue problem for not being a phrase10.7.1;
        }
    }
    return rv;

§10.2.1. A whole set of problem messages arise out of contextual failures:

Issue problem for not being an lvalue10.2.1 =

    THIS_IS_AN_ORDINARY_PROBLEM;
    Problems::quote_source(1, current_sentence);
    Problems::quote_wording(2, Node::get_text(p->down));
    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_ValueAsStorageItem));
    Problems::issue_problem_segment(
        "You wrote %1, but '%2' is a value, not a place where a value is "
        "stored. "
        "%PFor example, if 'The tally is a number that varies.', then "
        "I can 'increment the tally', but I can't 'increment 37' - the "
        "number 37 is always what it is. Similarly, I can't 'increment "
        "the number of people'. Phrases like 'increment' work only on "
        "stored values, like values that vary, or table entries.");
    Problems::issue_problem_end();
    return NEVER_MATCH;

§10.3.1. Issue problem for not being a table reference10.3.1 =

    THIS_IS_AN_ORDINARY_PROBLEM;
    Problems::quote_source(1, current_sentence);
    Problems::quote_wording(2, Node::get_text(p->down));
    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_ValueAsTableReference));
    Problems::issue_problem_segment(
        "You wrote %1, but '%2' is a value, not a reference to an entry "
        "in a table.");
    Problems::issue_problem_end();
    return NEVER_MATCH;

§10.4.1. Issue problem for not being an existing local10.4.1 =

    if (TEST_DASH_MODE(ISSUE_LOCAL_PROBLEMS_DMODE)) {
        THIS_IS_AN_ORDINARY_PROBLEM;
        Problems::quote_source(1, current_sentence);
        Problems::quote_wording(2, Node::get_text(p));
        if (Specifications::is_kind_like(p->down))
            Problems::quote_text(3, "a kind of value");
        else
            Problems::quote_kind_of(3, p->down);
        StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_ExistingVarNotFound));
        Problems::issue_problem_segment(
            "In the sentence %1, I was expecting that '%2' would be the "
            "name of a temporary value, but it turned out to be %3.");
        Problems::issue_problem_end();
    }
    return NEVER_MATCH;

§10.6.1. Issue problem for being the wrong rvalue10.6.1 =

    THIS_IS_AN_ORDINARY_PROBLEM;
    Problems::quote_source(1, current_sentence);
    Problems::quote_wording(2, Node::get_text(p->down));
    Problems::quote_spec(3, p->down);
    Problems::quote_spec(4, val);
    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_NotExactValueWanted));
    Problems::issue_problem_segment(
        "In the sentence %1, I was expecting that '%2' would be the specific "
        "value '%4'.");
    Problems::issue_problem_end();
    return NEVER_MATCH;

§10.7.1. Issue problem for not being a phrase10.7.1 =

    THIS_IS_AN_ORDINARY_PROBLEM;
    Problems::quote_source(1, current_sentence);
    Problems::quote_wording(2, Node::get_text(p->down));
    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(...));
    Problems::issue_problem_segment(
        "In the sentence %1, I was expecting that '%2' would be a phrase.");
    Problems::issue_problem_end();
    return NEVER_MATCH;

§10.8. New variables. The following doesn't switch context and recurse down: there's nothing to recurse down to, since all we have is a name for a new variable. Instead we deal with that right away.

It might seem rather odd that the typechecker should be the part of Inform which creates local variables. Surely that's a sign that the parsing went wrong, so how did things get to this stage?

In a C-like language, where variables are predeclared, that would be true. But in Inform, a phrase like:

let the monster be a random pterodactyl;

can be valid even where "the monster" is text not known to the S-parser as yet — indeed, that's how local variables are made. It's the typechecker which sorts this out, because only the typechecker can decide which of the subtly different forms of "let" is being used.

Deal with a new local variable name10.8 =

    kind *K = Node::get_kind_required_by_context(p);
    parse_node *check = p->down;
    if (Node::is(check, AMBIGUITY_NT)) check = check->down;
    if (LocalVariables::permit_as_new_local(check, FALSE)) {
        if (kind_of_var_to_create) *kind_of_var_to_create = K;
        return ALWAYS_MATCH;
    }
    Issue a problem for an inappropriate variable name10.8.1;
    return NEVER_MATCH;

§10.8.1. This problem message is never normally seen using the definitions in the Standard Rules because the definitions made there are such that other problems appear first. So the only way to see this message is to declare an unambiguous phrase with one of its tokens requiring a variable of a species; and then to misuse that phrase.

Issue a problem for an inappropriate variable name10.8.1 =

    THIS_IS_AN_ORDINARY_PROBLEM;
    Problems::quote_source(1, current_sentence);
    Problems::quote_wording(2, Node::get_text(p));
    if (Specifications::is_kind_like(p->down))
        Problems::quote_text(3, "a kind of value");
    else
        Problems::quote_kind_of(3, p->down);
    Problems::quote_kind(4, K);
    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_KindOfVariable));
    Problems::issue_problem_segment(
        "In the sentence %1, I was expecting that '%2' would be a new "
        "variable name (to hold %4), but it turned out to be %3.");
    Problems::issue_problem_end();

§10.9. (4) Typechecking within current context. Everything else, then, passes through here, with the context now set either to NULL (meaning no expectations) or to some ancestor of p in the parse tree.

Level 4 forks rapidly into three branches: (4A), for ambiguous readings; (4I), for single invocations; and (4S), for single readings other than invocations. Here's the code which does the switching:

Typecheck within current context10.9 =

    kind *kind_needed = NULL;
    int condition_context = FALSE;
    if (context) {
        kind_needed = Node::get_kind_required_by_context(context);
        if ((Node::is(context, CONDITION_CONTEXT_NT)) ||
            (Node::is(context, LOGICAL_AND_NT)) ||
            (Node::is(context, LOGICAL_OR_NT)) ||
            (Node::is(context, LOGICAL_NOT_NT)) ||
            (Node::is(context, LOGICAL_TENSE_NT)))
            condition_context = TRUE;
    }
    LOG_DASH("(4)");

    int outcome = ALWAYS_MATCH;
    if ((consider_alternatives) && (p->next_alternative))
        Resolve an ambiguous reading10.9.2
    else
        Verify an unambiguous reading10.9.1;
    return outcome;

§10.9.1. For a phrase node, we pass the buck down to its invocation list. For an invocation list, we pass the buck down to its invocation (which may or may not be the first in a chain of alternatives), which means we end up in (4I) either directly or via (4A). For everything else, it's (4S) for us.

Verify an unambiguous reading10.9.1 =

    switch (p->node_type) {
        case PHRASE_TO_DECIDE_VALUE_NT:
            outcome = Dash::typecheck_recursive(p->down, context, TRUE);
            break;

        case INVOCATION_LIST_NT: case INVOCATION_LIST_SAY_NT: case AMBIGUITY_NT:
            if (p->down == NULL) Unknown found text occurs as a command10.9.1.3;
            BEGIN_DASH_MODE;
            Dash_ambiguity_list = p;
            outcome = Dash::typecheck_recursive(p->down, context, TRUE);
            END_DASH_MODE;
            break;

        case INVOCATION_NT: Step (4I) Verify an invocation10.9.1.1; break;

        default: Step (4S) Verify anything else10.9.1.2; break;
    }

§10.9.2. (4A) Ambiguities. Ambiguities presently consist of chains of invocation nodes listed in the tree as alternatives.

Resolve an ambiguous reading10.9.2 =

    LOG_DASH("(4A)");
    parse_node *list_of_possible_readings[MAX_INVOCATIONS_PER_PHRASE];
    int no_of_possible_readings = 0;
    int no_of_passed_readings = 0;

    Step (4A.a) Set up the list of readings to test10.9.2.1;
    Step (4A.b) Recurse Dash to try each reading in turn10.9.2.2;
    if (Dash::problems_have_been_issued()) return NEVER_MATCH;
    if (no_of_passed_readings > 0) Step (4A.c) Preserve successful readings10.9.2.3
    else Step (4A.d) Give up with no readings possible10.9.2.4;
    LOGIF(MATCHING, "Ambiguity resolved to: $E", p);

§10.9.2.1. Phrase definitions are kept in a linked list with a total ordering which properly contains the partial ordering in which \(P_1\leq P_2\) if they are lexically identical and if each parameter of \(P_1\) provably, at compile time, also satisfies the requirements for the corresponding parameter of \(P_2\). They have already been lexically parsed in that order, so the list of invocations (which will have accumulated during parsing) is also in that same order. Now this is nearly the correct order for type-checking. But we make one last adjustment: the phrase being compiled is moved to the back of the list. This is to make recursion always the last thing checked, so that later rules can override earlier ones but still make use of them.

Step (4A.a) Set up the list of readings to test10.9.2.1 =

    LOG_DASH("(4A.a)");
    parse_node *alt;
    LOOP_THROUGH_ALTERNATIVES(alt, p)
        if ((Node::is(alt, INVOCATION_NT)) &&
            (Node::get_phrase_invoked(alt) != Functions::defn_being_compiled()))
            Add this reading to the list of test cases10.9.2.1.1;
    LOOP_THROUGH_ALTERNATIVES(alt, p)
        if (!((Node::is(alt, INVOCATION_NT)) &&
            (Node::get_phrase_invoked(alt) != Functions::defn_being_compiled())))
            Add this reading to the list of test cases10.9.2.1.1;
    LOGIF(MATCHING, "Resolving %d possible readings:\n", no_of_possible_readings);
    for (int i=0; i<no_of_possible_readings; i++)
        LOGIF(MATCHING, "Possibility (P%d) $e\n", i, list_of_possible_readings[i]);

§10.9.2.1.1. In general, it's not great for typecheckers in compilers to put an upper bound on complexity, because although human-written code seldom hits such maxima, there's always the possibility of mechanically-generated code which does. On the other hand, the result of that doctrine is that a lot of modern compilers (Swift, for example) slow to a painful crawl and allocate gigabytes of memory trying to understand strange type constraints in two or three lines of code. So, for now at least, let's be pragmatic.

Add this reading to the list of test cases10.9.2.1.1 =

    if (no_of_possible_readings >= MAX_INVOCATIONS_PER_PHRASE) {
        THIS_IS_AN_ORDINARY_PROBLEM;
        Problems::quote_wording(1, Node::get_text(p));
        StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_AmbiguitiesTooDeep));
        Problems::issue_problem_segment(
            "The phrase %1 is too complicated for me to disentangle without "
            "running very, very slowly as I check many ambiguities in it. There "
            "ought to be some way to simplify things for me?");
        Problems::issue_problem_end();
        return NEVER_MATCH;
    }
    list_of_possible_readings[no_of_possible_readings++] = alt;
    Dash::clear_flags(alt);

§10.9.2.2. Now we work through the list of tests. We must produce at least one reading passing at least at the "sometimes" level marked by the UNPROVEN_DASHFLAG, or else the whole specification fails its match. The first proven match stops our work, since we can never need lower-priority interpretations.

Step (4A.b) Recurse Dash to try each reading in turn10.9.2.2 =

    LOG_DASH("(4A.b)");
    for (int ref = 0; ref<no_of_possible_readings; ref++) {
        parse_node *inv = list_of_possible_readings[ref];

        Test the current reading and set its results flags accordingly10.9.2.2.1;
        LOGIF(MATCHING, "(P%d) %s: $e\n", ref, Dash::verdict_to_text(inv), inv);

        if (Dash::test_flag(inv, PASSED_DASHFLAG)) {
            no_of_passed_readings++;
            if (Dash::test_flag(inv, UNPROVEN_DASHFLAG) == FALSE) break;
        }
        if (Dash::problems_have_been_issued()) break;  to prevent duplication of problem messages
    }
    LOGIF(MATCHING, "List %s: ", (no_of_passed_readings > 0)?"passed":"failed");
    for (int i=0; i<no_of_possible_readings; i++) {
        parse_node *inv = list_of_possible_readings[i];
        LOGIF(MATCHING, "%s ", Dash::quick_verdict_to_text(inv));
    }
    LOGIF(MATCHING, "|\n");

§10.9.2.2.1. We tell Dash to run silently unless grosser-than-gross problems arise, and also tell it to check the reading with no alternatives considered. (If we let it consider alternatives, that would be circular: we'd end up here again, and so on forever.)

Test the current reading and set its results flags accordingly10.9.2.2.1 =

    LOGIF(MATCHING, "(P%d) Trying <%W>: $e\n", ref, Node::get_text(inv), inv);

    BEGIN_DASH_MODE;
    DASH_MODE_EXIT(ISSUE_PROBLEMS_DMODE);
    int rv = Dash::typecheck_recursive(inv, context, FALSE);
    END_DASH_MODE;

    Dash::set_flag(inv, TESTED_DASHFLAG);
    if (rv != NEVER_MATCH) {
        Dash::set_flag(inv, PASSED_DASHFLAG);
        outcome = Dash::worst_case(outcome, rv);
    }

§10.9.2.3. This is the happy ending, in which the list can probably be passed, though there are still a handful of pitfalls.

Step (4A.c) Preserve successful readings10.9.2.3 =

    LOG_DASH("(4A.c)");
    Step (4A.c.1) Winnow the reading list down to the survivors10.9.2.3.1;
    Step (4A.c.2) Infer the kind of any requested local variable10.9.2.3.2;

§10.9.2.3.1. To recap, after checking through the possible readings we have something like this as the result:

    f ? f g ? ? p - - -

We can now throw away the f, g and - readings — failed, grossly failed, or never reached — to leave just those which will be compiled:

    ? ? ? p

If compiled this will result in run-time code to check if the arguments allow the first invocation and run it if so; then the second; then the third; and, if those three fell through, run the fourth invocation without further checking.

Step (4A.c.1) Winnow the reading list down to the survivors10.9.2.3.1 =

    LOG_DASH("(4A.c.1)");
    int invocational = TRUE;
    if (Node::is(Dash_ambiguity_list, AMBIGUITY_NT)) invocational = FALSE;

    LOGIF(MATCHING, "Winnow %s from $T\n",
        (invocational)?"invocationally":"regularly", Dash_ambiguity_list);

    if (invocational) {
        int dubious = FALSE;
        for (int ref = 0; ref<no_of_possible_readings; ref++) {
            parse_node *inv = list_of_possible_readings[ref];
            if (Node::is(inv, INVOCATION_NT) == FALSE)
                dubious = TRUE;
        }
        if (dubious) Issue the dubious ambiguity problem message10.9.2.3.1.1;
    }

    if (invocational) Dash_ambiguity_list->down = NULL;

    parse_node *last_survivor = NULL;
    for (int ref = 0; ref<no_of_possible_readings; ref++) {
        parse_node *inv = list_of_possible_readings[ref];
        inv->next_alternative = NULL;
        if (Dash::test_flag(inv, PASSED_DASHFLAG)) {
            if (invocational) {
                if (last_survivor) last_survivor->next_alternative = inv;
                else Dash_ambiguity_list->down = inv;
                last_survivor = inv;
            } else {
                parse_node *link = Dash_ambiguity_list->next;
                Node::copy(Dash_ambiguity_list, inv);
                Dash_ambiguity_list->next = link;
                Dash_ambiguity_list->next_alternative = NULL;
                break;
            }
        }
    }

    if (invocational) {
        p = Dash_ambiguity_list->down;
        int nfi = -1, number_ambiguity = FALSE;
        parse_node *inv;
        LOOP_THROUGH_ALTERNATIVES(inv, p)
            if (Node::is(inv, INVOCATION_NT)) {
                int nti = Invocations::get_no_tokens(inv);
                if (nfi == -1) nfi = nti;
                else if (nfi != nti) number_ambiguity = TRUE;
            }

        if (number_ambiguity) Issue the number ambiguity problem message10.9.2.3.1.2;
    }
    LOGIF(MATCHING, "After winnowing, CS is $T\n", current_sentence);

§10.9.2.3.1.1. This is a last-throw-of-the-dice problem message, designed to pick up just a few really awkward ambiguities which have been missed elsewhere in the parser or in Dash.

Issue the dubious ambiguity problem message10.9.2.3.1.1 =

    THIS_IS_AN_ORDINARY_PROBLEM;
    Problems::quote_source(1, current_sentence);
    Problems::quote_number(2, &no_of_possible_readings);
    Problems::quote_wording(3, Node::get_text(list_of_possible_readings[0]));
    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_DubiousAmbiguity));
    Problems::issue_problem_segment(
        "The phrase %1 is ambiguous in a way that I can't sort out. "
        "I can see %2 different meanings of '%3', and no good way to choose.");
    Problems::issue_problem_end();
    return NEVER_MATCH;

§10.9.2.3.1.2. This is another sort of error which couldn't happen with a conventional programming language — in C, for instance, it's syntactically obvious how many arguments a function call has, because the brackets and commas are unambiguous. But in Inform, there are no reserved tokens of syntax acting like that. So we could easily have two accepted invocations in the list which have different numbers of arguments to each other, and there's no way safely to adjudicate that at run-time.

Issue the number ambiguity problem message10.9.2.3.1.2 =

    THIS_IS_AN_ORDINARY_PROBLEM;
    Problems::quote_source(1, current_sentence);
    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_UnequalValueAmbiguity));
    Problems::issue_problem_segment(
        "The phrase %1 is ambiguous in a way that I can't disentangle. "
        "It has more than one plausible interpretation, such that it "
        "would only be possible to tell which is valid at run-time: "
        "ordinarily that would be fine, but because the different "
        "interpretations are so different (and involve different "
        "numbers of values being used) there's no good way to cope. "
        "Try rewording one of the phrases which caused this clash: "
        "there's a good chance the problem will then go away.");
    Problems::issue_problem_end();
    return NEVER_MATCH;

§10.9.2.3.2. If an invocation passes, and asks to create a local variable, we need to mark the tree accordingly. If there's just one invocation then (4I) handles this, but if there's ambiguity, we handle it here, and only for the surviving nodes.

Step (4A.c.2) Infer the kind of any requested local variable10.9.2.3.2 =

    LOG_DASH("(4A.c.2)");
    parse_node *inv;
    LOOP_THROUGH_ALTERNATIVES(inv, p)
        if (Node::is(inv, INVOCATION_NT))
            if (Dash::set_up_any_local_required(inv) == NEVER_MATCH)
                return NEVER_MATCH;

§10.9.2.4. And this is the unhappy ending:

Step (4A.d) Give up with no readings possible10.9.2.4 =

    LOG_DASH("(4A.d)");
    THIS_IS_AN_ORDINARY_PROBLEM;
    if (InvocationLists::length(p) == 0) return NEVER_MATCH;

    LOGIF(MATCHING, "All possibilities failed: issuing problem\n");
    return Dash::failed(list_of_possible_readings, no_of_possible_readings,
        context, kind_needed);

§10.9.1.1. (4I) Invocations. Invocations are the hardest nodes to check, but here at least we can forget all about the ambiguities arising from multiple possibilities, and look at just a single one.

In the event of an interesting problem message, we mark an invocation as being interestingly problematic, but we keep going, since other invocations might be better. Only if everything fails will we retrace our steps and actually throw the problem.

Step (4I) Verify an invocation10.9.1.1 =

    LOG_DASH("(4I)");
    int no_gross_problems_thrown_before = no_gross_problems_thrown;
    int no_interesting_problems_thrown_before = no_interesting_problems_thrown;
    int qualified = FALSE;
    parse_node *inv = p;
    ParseInvocations::parse_within_inv(inv);
    Dash::set_flag(inv, TESTED_DASHFLAG);
    id_body *idb = Node::get_phrase_invoked(inv);
    if (idb) {
        Node::set_kind_resulting(inv, IDTypeData::get_return_kind(&(idb->type_data)));

         are the arguments of the right kind?
        if (outcome != NEVER_MATCH) Step (4I.a) Take care of arithmetic phrases10.9.1.1.2;
        if (outcome != NEVER_MATCH) Step (4I.b) Take care of non-arithmetic phrases10.9.1.1.3;
        if (outcome != NEVER_MATCH) Step (4I.c) Match type templates in the argument specifications10.9.1.1.4;
        if (outcome != NEVER_MATCH) Step (4I.d) Match kinds in assignment phrases10.9.1.1.5;

         if this evaluates something, is it a value of the right kind?
        if (outcome != NEVER_MATCH) Step (4I.e) Check kind of value returned10.9.1.1.6;

         are there any special rules about invoking this phrase?
        if (outcome != NEVER_MATCH) Step (4I.f) Check any phrase options10.9.1.1.7;
        if (outcome != NEVER_MATCH) Step (4I.g) Worry about self in say property of10.9.1.1.8;
        if (outcome != NEVER_MATCH) Step (4I.h) Worry about using a phrase outside of the control structure it belongs to10.9.1.1.9;
        if (outcome != NEVER_MATCH) Step (4I.i) Disallow any phrases which are now deprecated10.9.1.1.10;

         should we mark to create a let variable here?
        if ((outcome != NEVER_MATCH) && (consider_alternatives))
            outcome = Dash::worst_case(outcome, Dash::set_up_any_local_required(inv));
    }

     the outcome is now definitely known
    if (outcome == NEVER_MATCH) Step (4I.j) Cope with failure10.9.1.1.11
    else Step (4I.k) Cope with success10.9.1.1.12;

§10.9.1.1.1. Most problem messages issued by (4I) will be of a sort called "interesting", and will use the following macro.

define THIS_IS_AN_INTERESTING_PROBLEM
    outcome = NEVER_MATCH;
    no_interesting_problems_thrown++;
    if (TEST_DASH_MODE(ISSUE_INTERESTING_PROBLEMS_DMODE))

§10.9.1.1.2. "Polymorphic" here means that the phrase (i) produces a value, and (ii) that the kind of this value depends on the kinds of its arguments. Inform supports only a few polymorphic phrases, all clearly declared as such in the Standard Rules, and they come in two sorts: those marked with a "polymorphism exception", and those marked as "arithmetic operations".

Step (4I.a) Take care of arithmetic phrases10.9.1.1.2 =

    LOG_DASH("(4I.a)");
    if (IDTypeData::arithmetic_operation(idb) == TOTAL_OPERATION)
        Step (4I.a.1) "Total P of O" has kind the kind of P10.9.1.1.2.1
    else if (IDTypeData::is_arithmetic_phrase(idb)) Step (4I.a.2) Dimension-check arithmetic phrases10.9.1.1.2.2;

§10.9.1.1.2.1. For instance, the kind of "total carrying capacity of people in the Dining Room" is a number, because the kind of the property "carrying capacity" is "number".

Step (4I.a.1) "Total P of O" has kind the kind of P10.9.1.1.2.1 =

    LOG_DASH("(4I.a.1)");
    parse_node *P = Invocations::get_token_as_parsed(inv, 0);
    int rv = Dash::typecheck_recursive(P, NULL, TRUE);
    if ((rv != NEVER_MATCH) && (Rvalues::is_CONSTANT_construction(P, CON_property))) {
        property *prn = Rvalues::to_property(P);
        if (Properties::is_value_property(prn))
            Node::set_kind_resulting(inv, ValueProperties::kind(prn));
        else {
            THIS_IS_AN_INTERESTING_PROBLEM {
                StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_TotalEitherOr),
                    "this seems to be an attempt to total up an either/or property",
                    "and by definition such a property has nothing to total.");
            }
        }
    } else Fail the invocation for totalling something other than a property10.9.1.1.2.1.1;

§10.9.1.1.2.1.1. The problem message here is to help what turns out to be quite a popular mistake. (Perhaps we should simply implement column-totalling and be done with it.)

Fail the invocation for totalling something other than a property10.9.1.1.2.1.1 =

    LOG_DASH("(4I.a.1) failed as nonproperty");
    if (Kinds::get_construct(Node::get_kind_of_value(P)) == CON_table_column) {
        THIS_IS_AN_INTERESTING_PROBLEM {
            StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_TotalTableColumn),
                "this seems to be an attempt to total up the column of a table",
                "whereas it's only legal to use 'total' for properties.");
        }
    }
    outcome = NEVER_MATCH;

§10.9.1.1.2.2. For instance, the following blocks an attempt to add a number to a text.

Step (4I.a.2) Dimension-check arithmetic phrases10.9.1.1.2.2 =

    LOG_DASH("(4I.a.2)");
    int op_number = IDTypeData::arithmetic_operation(idb);
    LOGIF(MATCHING, "Arithmetic operation <op-%d>\n", op_number);

    parse_node *L, *R;
    kind *kind_wanted, *left_kind, *right_kind, *kind_produced;
    Work out the kinds of the operands, and what we want, and what we get10.9.1.1.2.2.1;
    LOGIF(MATCHING, "%u (~) %u = %u\n", left_kind, right_kind, kind_produced);

    if (kind_produced) Node::set_kind_resulting(inv, kind_produced);
    else Fail the invocation for a dimensional problem10.9.1.1.2.2.2;

§10.9.1.1.2.2.1. For the way this is actually worked out, see the section on "Dimensions".

Work out the kinds of the operands, and what we want, and what we get10.9.1.1.2.2.1 =

    L = Invocations::get_token(inv, 0);
    left_kind = Dash::fix_arithmetic_operand(L);
    if (Kinds::Dimensions::arithmetic_op_is_unary(op_number)) {
        R = NULL; right_kind = NULL;
    } else {
        R = Invocations::get_token(inv, 1);
        right_kind = Dash::fix_arithmetic_operand(R);
    }
    if (((left_kind) && (Kinds::Behaviour::is_quasinumerical(left_kind) == FALSE)) ||
        ((right_kind) && (Kinds::Behaviour::is_quasinumerical(right_kind) == FALSE)))
        kind_produced = NULL;
    else
        kind_produced = Kinds::Dimensions::arithmetic_on_kinds(left_kind, right_kind, op_number);
    kind_wanted = kind_needed;

§10.9.1.1.2.2.2. Note that "value" — the vaguest kind of all — might come up here as a result of some problem evaluating one of the operands, which has already been reported in a problem message; so we only issue this problem message when L and R are more definite.

Fail the invocation for a dimensional problem10.9.1.1.2.2.2 =

    if ((left_kind) && (Kinds::eq(left_kind, K_value) == FALSE) &&
        (right_kind) && (Kinds::eq(right_kind, K_value) == FALSE)) {
        THIS_IS_AN_INTERESTING_PROBLEM {
            LOG("So the inv subtree is:\n$T\n", inv);
            Problems::quote_source(1, current_sentence);
            Problems::quote_wording(2, Node::get_text(L));
            Problems::quote_wording(3, Node::get_text(R));
            Problems::quote_kind(4, left_kind);
            Problems::quote_kind(5, right_kind);
            switch(op_number) {
                case PLUS_OPERATION: Problems::quote_text(6, "adding"); Problems::quote_text(7, "to"); break;
                case MINUS_OPERATION: Problems::quote_text(6, "subtracting"); Problems::quote_text(7, "from"); break;
                case TIMES_OPERATION: Problems::quote_text(6, "multiplying"); Problems::quote_text(7, "by"); break;
                case DIVIDE_OPERATION:
                case REMAINDER_OPERATION: Problems::quote_text(6, "dividing"); Problems::quote_text(7, "by"); break;
                case APPROXIMATION_OPERATION: Problems::quote_text(6, "rounding"); Problems::quote_text(7, "to"); break;
            }
            StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_BadArithmetic));
            Problems::issue_problem_segment(
                "You wrote %1, but that seems to involve %6 %4 ('%2') %7 %5 ('%3'), "
                "which is not good arithmetic.");
            Problems::issue_problem_end();
        }
    }
    outcome = NEVER_MATCH;

§10.9.1.1.3. This is the general case: almost all phrases fall into this category, including all phrases created outside the Standard Rules.

The deal is simply that every argument must match its specification. For instance, if inv is an invocation of this phrase:

To truncate (L - a list of values) to (N - a number) entries: ...

...then token 0 must match "list of values", and token 1 must match "number".

Step (4I.b) Take care of non-arithmetic phrases10.9.1.1.3 =

    if (IDTypeData::is_arithmetic_phrase(idb) == FALSE) {
        LOG_DASH("(4I.b)");
        int i, exit_at_once = FALSE;
        for (i=0; i<Invocations::get_no_tokens(inv); i++) {
            LOGIF(MATCHING, "(4I.b) trying argument %d (prior to this, best possible: %d)\n",
                i, outcome);
            Invocations::set_token_check_to_do(inv, i, NULL);
            Type-check a single token from the list10.9.1.1.3.1;
            if (exit_at_once) break;
        }
        LOGIF(MATCHING, "(4I.b) argument type matching %s\n",
            (outcome==NEVER_MATCH)?"failed":"passed");
    }

§10.9.1.1.3.1. Type-check a single token from the list10.9.1.1.3.1 =

    parse_node *ith_spec = idb->type_data.token_sequence[i].to_match;
    if ((idb->type_data.token_sequence[i].construct == KIND_NAME_IDTC) && (outcome != NEVER_MATCH))
        Cautiously reparse this as a name of a kind of value10.9.1.1.3.1.1
    else {
        int save_kcm = kind_checker_mode;
        kind_checker_mode = MATCH_KIND_VARIABLES_AS_UNIVERSAL;
        kind *create = NULL;
        BEGIN_DASH_MODE;
        DASH_MODE_EXIT(ISSUE_PROBLEMS_DMODE);
        DASH_MODE_CREATE(&create);
        int rv = Dash::typecheck_recursive(Invocations::get_token(inv, i), context, TRUE);
        END_DASH_MODE;
        switch(rv) {
            case NEVER_MATCH:
                LOGIF(MATCHING, "(4I.b) on %W failed at token %d\n", Node::get_text(p), i);
                outcome = NEVER_MATCH;
                if (Dash::problems_have_been_issued()) exit_at_once = TRUE;
                break;
            case SOMETIMES_MATCH:
                LOGIF(MATCHING, "(4I.b) on %W qualified at token %d\n", Node::get_text(p), i);
                Invocations::set_token_check_to_do(inv, i, ith_spec);
                qualified = TRUE;
                break;
        }
        kind_checker_mode = save_kcm;
        if (create) {
            if ((CompileBlocksAndLines::compiling_single_line_block()) &&
                (IDTypeData::is_a_let_assignment(idb))) {
                THIS_IS_AN_INTERESTING_PROBLEM {
                    Problems::quote_source(1, current_sentence);
                    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_LetCreatedInIf));
                    Problems::issue_problem_segment(
                        "You wrote %1, but when a temporary value is created "
                        "inside an 'if ..., ...' or an 'otherwise ...', it only "
                        "lasts until that line is complete - which means it "
                        "can never be used for anything, because it goes away "
                        "as soon as created. To make something more durable, "
                        "create it before the 'if' or 'otherwise'.");
                    Problems::issue_problem_end();
                }
            }
            Invocations::set_token_variable_kind(inv, i, create);
        }
    }

§10.9.1.1.3.1.1. The following is a delicate manoeuvre, but luckily it takes action only very rarely and in very specific circumstances. We allow a very limited use of second-order logic in using the name of a kind as if it were a value, even though Inform is really not set up for this. The point is to allow:

let (name - nonexisting variable) be (K - name of kind of word value);

where the "K" parameter would match (1) but not (2), (3) or (4) from:

(1) let X be a number;

(2) let X be text;

(3) let X be 21;

(4) let X be \{1, 2, 3\};

What all of this has to do with being UNKNOWN_NT is that text parsed in the expectation of a value will usually not recognise something like "a list of numbers", so that would be here as UNKNOWN_NT. We take the otherwise unheard-of measure of reparsing the text, but we only impose the result if the match can definitely be made successfully.

We have to be very careful to take action only on (5) and not (6):

(5) let L be a list of scenes;

(6) let L be the list of scenes;

Cautiously reparse this as a name of a kind of value10.9.1.1.3.1.1 =

    outcome = NEVER_MATCH;
    parse_node *ith_token = Invocations::get_token_as_parsed(inv, i);
    LOGIF(MATCHING, "(4I.b) thinking about reparsing: $P\n", ith_token);
    int warned_already = FALSE;
    if (Node::is(ith_token, AMBIGUITY_NT)) ith_token = ith_token->down;
    if ((Node::is(ith_token, UNKNOWN_NT)) ||
        (Specifications::is_description(ith_token)) ||
        (Rvalues::is_CONSTANT_construction(ith_token, CON_property)) ||
        (Specifications::is_kind_like(ith_token)) ||
        ((IDTypeData::is_a_let_assignment(idb) == FALSE) &&
            (Node::is(ith_token, PHRASE_TO_DECIDE_VALUE_NT)))) {
        wording W = Node::get_text(ith_token);

        kind *K = NULL;
        parse_node *reparsed = NULL;
        if (<s-type-expression>(W)) reparsed = <<rp>>;
        if (Specifications::is_kind_like(reparsed))
            K = Specifications::to_kind(reparsed);
        if ((K == NULL) && (<k-kind>(W))) K = <<rp>>;
        if (K == NULL) {
            if ((<value-property-name>(W)) &&
                (ValueProperties::coincides_with_kind(<<rp>>)))
                K = ValueProperties::kind(<<rp>>);
        }

        LOGIF(MATCHING, "(4I.b) reparsed as: %u (vs spec $P)\n", K, ith_spec);
        if ((K) && (Specifications::is_kind_like(ith_spec))) {
            kind *ikind = Specifications::to_kind(ith_spec);
            if (Kinds::Behaviour::definite(K)) {
                if (Kinds::compatible(K, ikind) == ALWAYS_MATCH) {
                    LOGIF(MATCHING, "(4I.b) allows name-of token: $P\n", reparsed);
                    Invocations::set_token_as_parsed(inv, i, Node::duplicate(reparsed));
                    outcome = ALWAYS_MATCH;
                } else {
                    THIS_IS_AN_ORDINARY_PROBLEM {
                        warned_already = TRUE;
                        Problems::quote_source(1, current_sentence);
                        Problems::quote_wording(2, W);
                        Problems::quote_kind(3, ikind);
                        StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_NameOfKindMismatch));
                        Problems::issue_problem_segment(
                            "You wrote %1, but although '%2' is the name of a kind, "
                            "it isn't the name of a kind of %3, which this phrase needs.");
                        Problems::issue_problem_end();
                    }
                }
            } else {
                THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM {
                    warned_already = TRUE;
                    Problems::quote_source(1, current_sentence);
                    Problems::quote_wording(2, W);
                    Problems::quote_kind(3, ikind);
                    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_BadLocalKOV));
                    Problems::issue_problem_segment(
                        "You wrote %1, but although '%2' is the name of a kind, "
                        "it isn't a definite kind and is instead a general "
                        "description which might apply to many different kinds. "
                        "(For example, 'let R be a relation' is vague because it doesn't "
                        "make clear what R will relate - 'let R be a relation of numbers' "
                        "would be fine.)");
                    Problems::issue_problem_end();
                }
            }
        }
    }
    ith_token = Invocations::get_token_as_parsed(inv, i);
    if ((!Specifications::is_kind_like(ith_token)) && (warned_already == FALSE)) {
        THIS_IS_AN_ORDINARY_PROBLEM {
            Problems::quote_source(1, current_sentence);
            Problems::quote_wording(2, Node::get_text(ith_token));
            StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_NameOfKindIsnt));
            Problems::issue_problem_segment(
                "You wrote %1, but although '%2' does have a meaning, "
                "it isn't the name of a kind, which this phrase needs.");
            Problems::issue_problem_end();
        }
    }

§10.9.1.1.4. For templates and the meaning of kind_checker_mode, see the section on "Kind Checking". But basically this handles the matching of an invocation against a definition like:

To remove (N - value of kind K) from (L - list of Ks): ...

Step (4I.c) Match type templates in the argument specifications10.9.1.1.4 =

    LOG_DASH("(4I.c)");
    int exit_at_once = FALSE;

    kind_variable_declaration *kvd_marker = LAST_OBJECT(kind_variable_declaration);

    if (IDTypeData::contains_variables(&(idb->type_data))) {
        kind_variable_declaration *save_most_recent_interpretation = most_recent_interpretation;
        int pass, save_kcm = kind_checker_mode;
        for (pass = 1; pass <= 2; pass++) {
            LOGIF(MATCHING, "(4I.c) prototype check pass %d\n", pass);
            if (Log::aspect_switched_on(MATCHING_DA)) Latticework::show_variables();
            if (pass == 1) kind_checker_mode = MATCH_KIND_VARIABLES_INFERRING_VALUES;
            else kind_checker_mode = MATCH_KIND_VARIABLES_AS_VALUES;
            int i;
            for (i=0; i<Invocations::get_no_tokens(inv); i++) {
                kind *Kt = IDTypeData::token_kind(&(idb->type_data), i);
                if ((Kt) &&
                    (idb->type_data.token_sequence[i].construct != NEW_LOCAL_IDTC)) {
                    parse_node *token_spec = Invocations::get_token_as_parsed(inv, i);
                    kind *kind_read = Specifications::to_kind(token_spec);
                    LOGIF(MATCHING, "Token %d: $P: kind %u: template %u\n", i,
                        token_spec, kind_read, Kt);
                    switch(Kinds::compatible(kind_read, Kt)) {
                        case NEVER_MATCH:
                            LOGIF(MATCHING, "(4I.c) failed at token %d\n", i);
                            outcome = NEVER_MATCH;
                            if (Dash::problems_have_been_issued()) exit_at_once = TRUE;
                            break;
                        case SOMETIMES_MATCH:
                            outcome = Dash::worst_case(outcome, SOMETIMES_MATCH);
                             we won't use with_qualifications — we don't know exactly what they are
                            LOGIF(MATCHING, "(4I.c) dropping to sometimes at token %d\n", i);
                            break;
                        case ALWAYS_MATCH:
                            break;
                    }
                }
                if (exit_at_once) break;
            }
            if (exit_at_once) break;
            if ((pass == 1) && (outcome != NEVER_MATCH)) {
                LOGIF(MATCHING, "(4I.c) prototype check passed\n");
                most_recent_interpretation = NULL;
                kind_variable_declaration *kvdm = kvd_marker;
                if (kvdm) kvdm = NEXT_OBJECT(kvdm, kind_variable_declaration);
                else kvdm = FIRST_OBJECT(kind_variable_declaration);
                while (kvdm) {
                    kvdm->next = most_recent_interpretation;
                    most_recent_interpretation = kvdm;
                    kvdm = NEXT_OBJECT(kvdm, kind_variable_declaration);
                }
            }
        }
        kind_checker_mode = save_kcm;
        if (outcome != NEVER_MATCH) Node::set_kind_variable_declarations(inv, most_recent_interpretation);
        most_recent_interpretation = save_most_recent_interpretation;
    }

    if (Kinds::contains(Node::get_kind_resulting(inv), CON_KIND_VARIABLE)) {
        int changed = FALSE;
        kind *K = Kinds::substitute(Node::get_kind_resulting(inv), NULL, &changed, FALSE);
        if (changed) {
            LOGIF(MATCHING, "(4I.c) amended kind returned to %u\n", K);
            Node::set_kind_resulting(inv, K);
        } else Disallow an undeclared kind variable as return kind10.9.1.1.4.1;
    }

§10.9.1.1.4.1. Disallow an undeclared kind variable as return kind10.9.1.1.4.1 =

    THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
    Problems::quote_source(1, current_sentence);
    Problems::quote_wording(2, Node::get_text(p));
    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(BelievedImpossible));
    Problems::issue_problem_segment(
        "In the line %1, you seem to be using '%2' to produce a value, but "
        "it's not clear what kind of value this will be. It seems to use "
        "a phrase which has been declared wrongly, because the kind it decides "
        "is given only by a symbol which isn't otherwise defined.");
    Problems::issue_problem_end();
    outcome = NEVER_MATCH;

§10.9.1.1.5. Although we don't implement it with prototypes as such, there's a similar constraint on the arguments of an assignment. If we are checking an invocation against:

To let (t - existing variable) be (u - value): ...

then we have so far checked that argument 0 is indeed the name of a variable which already exists. But suppose the invocation is

let N be "there'll be no mutant enemy";

where N has already been created as a variable of kind "number". This clearly has to be rejected, as it would violate type-safety. Step (4I.d) therefore makes sure that all assignments match the kind of the new value against the kind of the storage item to which it is being written.

A reasonable question might be why we don't implement this using the prototype system of (4I.c), thus removing a rule from this already-complex algorithm, say by

To let (var - K variable) be (val - value of kind K): ...

The answer is that this would indeed work nicely for valid source text, but that we would get less helpful problem messages in the all-too-likely case of a mistake having been made.

Step (4I.d) Match kinds in assignment phrases10.9.1.1.5 =

    LOG_DASH("(4I.d)");
    if (IDTypeData::is_assignment_phrase(idb)) {
        parse_node *target = Invocations::get_token_as_parsed(inv, 0);
        parse_node *new_value = Invocations::get_token_as_parsed(inv, 1);

        parse_node *target_spec = idb->type_data.token_sequence[0].to_match;
        parse_node *new_value_spec = idb->type_data.token_sequence[1].to_match;

        local_variable *lvar = Lvalues::get_local_variable_if_any(target);
        if ((lvar) && (LocalVariables::protected(lvar)))
            outcome = NEVER_MATCH;
        else {
            if (Kinds::Behaviour::is_object(Specifications::to_kind(target_spec)))
                Step (4I.d.1) Police an assignment to an object10.9.1.1.5.1;

            if (idb->type_data.token_sequence[0].construct != NEW_LOCAL_IDTC)
                Step (4I.d.2) Police an assignment to a storage item10.9.1.1.5.2;
        }
    }

§10.9.1.1.5.1. It doesn't always look like an assignment, but a phrase such as:

change the Marble Door to open;

has similar type-checking needs.

Step (4I.d.1) Police an assignment to an object10.9.1.1.5.1 =

    LOG_DASH("(4I.d.1)");
    instance *target_wo = Rvalues::to_object_instance(target);
    property *prn = NULL;
    int make_check = FALSE;
    if (Kinds::eq(Node::get_kind_of_value(new_value_spec), K_value))
        Maybe we're changing an object to a value of a kind coinciding with a property10.9.1.1.5.1.1;
    if (Rvalues::is_CONSTANT_construction(new_value_spec, CON_property))
        Maybe we're changing an object to a named either/or property or condition state10.9.1.1.5.1.2;
    if (make_check)
        Check that the property exists and that the object is allowed to have it10.9.1.1.5.1.3;

§10.9.1.1.5.1.1. There are actually two definitions like this in the Standard Rules:

(1) To change (o - object) to (w - value): ...

(2) To change (o - object) to (p - property): ...

Here's the code for (1), the less obvious case. This is needed for something like

change the canvas to blue;

where "blue" is a constant colour, and "colour" is both a kind and also a property. (This case really is an assignment — it assigns the value "blue" to the colour property of the canvas.)

Maybe we're changing an object to a value of a kind coinciding with a property10.9.1.1.5.1.1 =

    LOG_DASH("(4I.d.1.a)");
    instance *I = Rvalues::to_instance(new_value);
    if (I == NULL) outcome = NEVER_MATCH;
    else {
        prn = Properties::property_with_same_name_as(Instances::to_kind(I));
        if (prn == NULL) outcome = NEVER_MATCH;
        else make_check = TRUE;
    }

§10.9.1.1.5.1.2. And here's the simpler case, (2). A small quirk here is that it will also pick up "change the Atrium to spiffy" in the following:

Atrium is a room. The Atrium can be spiffy, cool or lame.

When play begins: change the Atrium to spiffy.

...where "spiffy" is deemed a property rather than a constant value of a kind because of the way the condition of the Atrium is declared. This is a little bit horrid, but works fine in practice. (If we try to accommodate this case within (1.2.4.1a), which might seem more logical, we run into trouble because the property name is cast to a property value of self when being typechecked against "value".)

Maybe we're changing an object to a named either/or property or condition state10.9.1.1.5.1.2 =

    LOG_DASH("(4I.d.1.b)");
    if (Rvalues::is_CONSTANT_construction(new_value, CON_property))
        prn = Rvalues::to_property(new_value);
    else if (Descriptions::number_of_adjectives_applied_to(new_value) == 1) {
        adjective *aph = AdjectivalPredicates::to_adjective(Descriptions::first_unary_predicate(new_value));
        if (AdjectiveAmbiguity::has_enumerative_meaning(aph))
            prn = Properties::property_with_same_name_as(Instances::to_kind(AdjectiveAmbiguity::has_enumerative_meaning(aph)));
        else if (AdjectiveAmbiguity::has_either_or_property_meaning(aph, NULL))
            prn = AdjectiveAmbiguity::has_either_or_property_meaning(aph, NULL);
    }
    make_check = TRUE;

§10.9.1.1.5.1.3. We do something quite interesting here, if the object is not explicitly named: we deliberately allow an assignment which may not be type-safe, and without even dropping to the "sometimes" level. This is for phrases like so:

change the item to closed;

Here the author seems to know what he's doing, and is pretty sure that the current contents of "item" will accept closure. All we can prove is that "item" contains an object (or perhaps nothing, the non-object). But we allow the assignment because it will compile to code which will issue a helpful run-time problem message if it goes wrong.

Now that "change" has been removed, as of January 2011, it looks as if this case in the type-checker is never exercised.

Check that the property exists and that the object is allowed to have it10.9.1.1.5.1.3 =

    LOGIF(MATCHING, "Property appears to be $Y\n", prn);
    if (prn == NULL) {
        THIS_IS_AN_INTERESTING_PROBLEM {
            Problems::quote_source(1, current_sentence);
            Problems::quote_wording(2, Node::get_text(target));
            Problems::quote_wording(3, Node::get_text(new_value));
            StandardProblems::handmade_problem(Task::syntax_tree(), _p_(BelievedImpossible));  the parser seems not to allow these
            Problems::issue_problem_segment(
                "You wrote %1, asking to change the object '%2'. This would "
                "make sense if '%3' were an either/or property like 'open' "
                "(or perhaps a named property value like 'blue') - but it "
                "isn't, so the change makes no sense.");
            Problems::issue_problem_end();
        }
    }
    if ((target_wo) && (prn) &&
        (PropertyPermissions::find(Instances::as_subject(target_wo), prn, TRUE) == NULL)) {
        THIS_IS_AN_INTERESTING_PROBLEM {
            Problems::quote_source(1, current_sentence);
            Problems::quote_wording(2, Node::get_text(target));
            Problems::quote_property(3, prn);
            StandardProblems::handmade_problem(Task::syntax_tree(), _p_(BelievedImpossible));
            Problems::issue_problem_segment(
                "You wrote %1, but '%2' is not allowed to have the property '%3'.");
            Problems::issue_problem_end();
        }
    }

§10.9.1.1.5.2. This is more straightforward, with just a tiny glitch to make the rules tougher on variables which hold text to be parsed. (Because the regular rules for exchanging the subtly different forms of text which a double-quoted literal can mean are too generous.)

Step (4I.d.2) Police an assignment to a storage item10.9.1.1.5.2 =

    kind *kind_wanted, *kind_found;
    LOGIF(MATCHING, "Check assignment of $P to $P\n", new_value, target);
    switch(Lvalues::get_storage_form(target_spec)) {
        case LOCAL_VARIABLE_NT: Problems::quote_text(6, "the name of"); break;
        case PROPERTY_VALUE_NT: Problems::quote_text(6, "a property whose kind of value is"); break;
        case NONLOCAL_VARIABLE_NT: Problems::quote_text(6, "a variable whose kind of value is"); break;
        case TABLE_ENTRY_NT: Problems::quote_text(6, "a table entry whose kind of value is"); break;
        case LIST_ENTRY_NT: Problems::quote_text(6, "an entry in a list whose kind of value is"); break;
        default: Problems::quote_text(6, "a stored value holding"); break;
    }
    kind_wanted = Specifications::to_kind(target);
    kind_found = Specifications::to_kind(new_value);

    parse_node *new_invl = new_value->down;
    if (Node::is(new_invl, INVOCATION_LIST_NT)) {
        parse_node *new_inv;
        LOOP_THROUGH_ALTERNATIVES(new_inv, new_invl)
            if (Dash::test_flag(new_inv, PASSED_DASHFLAG)) break;
        if (new_inv) kind_found = Node::get_kind_resulting(new_inv);
    }
    LOGIF(MATCHING, "Kinds found: %u, wanted: %u\n", kind_found, kind_wanted);

    if (((K_understanding) && (Kinds::eq(kind_wanted, K_understanding)) &&
        (Kinds::eq(kind_found, K_understanding) == FALSE))
        || (Kinds::compatible(kind_found, kind_wanted) == NEVER_MATCH)) {
        THIS_IS_AN_INTERESTING_PROBLEM {
            Problems::quote_source(1, current_sentence);
            Problems::quote_wording(2, Node::get_text(target));
            Problems::quote_kind(3, Specifications::to_kind(target));
            Problems::quote_wording(4, Node::get_text(new_value));
            Problems::quote_kind(5, Specifications::to_kind(new_value));
            StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_ChangeToWrongValue));
            Problems::issue_problem_segment(
                "You wrote %1, but '%2' is supposed to be "
                "%6 %3, so it cannot be set equal to %4, whose kind is %5.");
            Problems::issue_problem_end();
        }
    }

§10.9.1.1.6. Suppose we have something like this:

award the current action points;

and we are typechecking found as "the current action" (a phrase deciding a value) against expected as "number", the parameter expected in "award (N - a number) points".

No matter how peculiar this invocation of found was, we have now successfully worked out the kind of the value it would return if compiled, and this is stored in inv->kind_resulting. We now check to see if this matches the kind expected — in this example, it won't, because a stored action does not cast to a number.

Step (4I.e) Check kind of value returned10.9.1.1.6 =

    LOG_DASH("(4I.e)");
    int outcome_test = ALWAYS_MATCH;
    if (kind_needed) {
        LOGIF(MATCHING, "Checking returned %u against desired %u\n",
            Node::get_kind_resulting(inv), kind_needed);
        outcome_test = Kinds::compatible(
            Node::get_kind_resulting(inv), kind_needed);
    }
    switch (outcome_test) {
        case NEVER_MATCH: outcome = NEVER_MATCH; break;
        case SOMETIMES_MATCH: outcome = Dash::worst_case(outcome, SOMETIMES_MATCH); break;
    }

§10.9.1.1.7. The final stage in type-checking a phrase is to ensure that any phrase options are properly used.

Step (4I.f) Check any phrase options10.9.1.1.7 =

    LOG_DASH("(4I.f)");
    if ((outcome != NEVER_MATCH) && (Node::get_phrase_options_invoked(inv))) {
        int cso = PhraseOptions::parse_invoked_options(
            inv, (TEST_DASH_MODE(ISSUE_PROBLEMS_DMODE))?FALSE:TRUE);
        if (cso == FALSE) outcome = NEVER_MATCH;
    }

§10.9.1.1.8. A say phrase which involves a property of something implicitly changes the scope for any vaguely described properties within the text supplied as that property (if it is indeed text). We have to mark any such property, and any such say. For instance, suppose we are typechecking

(1) "Oh, look: [initial appearance of the escritoire]"

and the initial appearance in question is:

(2) "A small, portable writing desk holding up to [carrying capacity] letters."

Printing text (2), it's important for the self object to be the escritoire, which might not be the case otherwise; so during the printing of (1), we have to change self temporarily and restore it afterwards.

Step (4I.g) Worry about self in say property of10.9.1.1.8 =

    LOG_DASH("(4I.g)");
    if ((IDTypeData::is_a_say_phrase(idb)) &&
        (Invocations::get_no_tokens(inv) == 1) &&
        (Lvalues::get_storage_form(Invocations::get_token_as_parsed(inv, 0)) == PROPERTY_VALUE_NT)) {
        Annotations::write_int(Invocations::get_token_as_parsed(inv, 0), record_as_self_ANNOT, TRUE);
        Invocations::mark_to_save_self(inv);
    }

§10.9.1.1.9. Some phrases are defined with a notation making them allowable only inside loops, or other control structures; for instance,

To break -- in loop: ...

And here is where we check that "break" is indeed used only in a loop.

Step (4I.h) Worry about using a phrase outside of the control structure it belongs to10.9.1.1.9 =

    LOG_DASH("(4I.h)");
    if (idb) {
        inchar32_t *required = IDTypeData::only_in(idb);
        if (required) {
            if (Wide::cmp(required, U"loop") == 0) {
                LOGIF(MATCHING, "Required to be inside loop body\n");
                if (CodeBlocks::inside_a_loop_body() == FALSE) {
                    THIS_IS_AN_INTERESTING_PROBLEM {
                        Problems::quote_source(1, current_sentence);
                        StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_CantUseOutsideLoop));
                        Problems::issue_problem_segment(
                            "%1 makes sense only inside a 'while' or 'repeat' loop.");
                        Problems::issue_problem_end();
                    }
                }
            } else {
                LOGIF(MATCHING, "Required to be inside block '%w'\n", required);
                inchar32_t *actual = CodeBlocks::name_of_current_block();
                if ((actual) && (Wide::cmp(actual, U"unless") == 0)) actual = U"if";
                if ((actual == NULL) || (Wide::cmp(required, actual) != 0)) {
                    THIS_IS_AN_INTERESTING_PROBLEM {
                        Problems::quote_source(1, current_sentence);
                        Problems::quote_wide_text(2, required);
                        StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_CantUseOutsideStructure));
                        Problems::issue_problem_segment(
                            "%1 makes sense only inside a '%2' block.");
                        Problems::issue_problem_end();
                    }
                }
            }
        }
    }

§10.9.1.1.10. Step (4I.i) Disallow any phrases which are now deprecated10.9.1.1.10 =

    if (global_compilation_settings.no_deprecated_features) {
        LOG_DASH("(4I.i)");
        if ((idb) && (idb->type_data.now_deprecated)) {
            THIS_IS_AN_INTERESTING_PROBLEM {
                Problems::quote_source(1, current_sentence);
                StandardProblems::handmade_problem(Task::syntax_tree(), _p_(BelievedImpossible));  too moving a target to test
                Problems::issue_problem_segment(
                    "'%1' uses a phrase which is now deprecated: you should rephrase "
                    "to avoid the need for it. I'd normally allow this, but you have "
                    "the 'Use no deprecated features' option set.");
                Problems::issue_problem_end();
            }
        }
    }

§10.9.1.1.11. Step (4I.j) Cope with failure10.9.1.1.11 =

    LOG_DASH("(4I.j) failure");
    if (no_gross_problems_thrown > no_gross_problems_thrown_before)
        Dash::set_flag(inv, GROSSLY_FAILED_DASHFLAG);
    else if (no_interesting_problems_thrown > no_interesting_problems_thrown_before)
        Dash::set_flag(inv, INTERESTINGLY_FAILED_DASHFLAG);
    if ((consider_alternatives) && (TEST_DASH_MODE(ISSUE_PROBLEMS_DMODE)))
        Dash::failed_one(inv, context, kind_needed);

§10.9.1.1.12. Usage statistics are mainly interesting to the writers of Inform, to help us to get some picture of how much phrases are used across a large corpus of existing source text (e.g., the documentation examples, or the public extensions).

Step (4I.k) Cope with success10.9.1.1.12 =

    LOG_DASH("(4I.k) success");
    Dash::set_flag(inv, PASSED_DASHFLAG);
    if (qualified) {
        Dash::set_flag(inv, UNPROVEN_DASHFLAG);
        Invocations::mark_unproven(inv);
    }
    if (idb) {
        wording NW = ToPhraseFamily::doc_ref(idb->head_of_defn);
        if (Wordings::nonempty(NW)) {
            TEMPORARY_TEXT(pds)
            WRITE_TO(pds, "%+W", Wordings::one_word(Wordings::first_wn(NW)));
            if (Log::aspect_switched_on(PHRASE_USAGE_DA)) {
                DocReferences::doc_mark_used(pds,
                    Wordings::first_wn(Node::get_text(inv)));
            }
            DISCARD_TEXT(pds)
        }
    }

§10.9.1.2. (4S) Verifying single non-invocation readings. This is much easier, though that's because a lot of the work is delegated to level 5.

Step (4S) Verify anything else10.9.1.2 =

    LOG_DASH("(4S.a)");
    LOG_INDENT;

    outcome = Dash::typecheck_single_node(p, kind_needed, condition_context);
    LOG_OUTDENT;
    Allow listed-in table references only where these are expected10.9.1.2.1;

    LOG_DASH("(4S.b)");
    for (parse_node *arg = p->down; arg; arg = arg->next)
        outcome =
            Dash::worst_case(outcome,
                Dash::typecheck_recursive(arg, p, TRUE));
    if ((outcome != NEVER_MATCH) && (p->down)) {
        if (Node::is(p, LIST_ENTRY_NT))
            Step (4S.c) Check arguments of a list entry10.9.1.2.2;
        if (Node::is(p, PROPERTY_VALUE_NT))
            Step (4S.d) Check arguments of a property value10.9.1.2.3;
        if (Node::is(p, TABLE_ENTRY_NT))
            Step (4S.e) Check arguments of a table reference10.9.1.2.4;
    }

§10.9.1.2.1. The "C listed in T" form of table reference is illegal as a general value, and allowed only in phrases using the table-reference token.

Allow listed-in table references only where these are expected10.9.1.2.1 =

    if ((Node::is(p, TABLE_ENTRY_NT)) &&
        (Node::no_children(p) == 2) &&
        (kind_needed) &&
        (!(Node::is(context, LVALUE_TR_CONTEXT_NT)))) {
        THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
        StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_InexplicitTableEntryAsValue),
            "this form of table entry can only be used in certain special phrases",
            "because it doesn't explicitly refer to a single value. (You can see "
            "which phrases in the Phrasebook index: it's allowed wherever a 'table "
            "entry' is wanted.)");
        return NEVER_MATCH;
    }

§10.9.1.2.2. For a list entry, we have to have a list and an index.

Step (4S.c) Check arguments of a list entry10.9.1.2.2 =

    LOG_DASH("(4S.c)");
    kind *K1 = Specifications::to_kind(p->down);
    kind *K2 = Specifications::to_kind(p->down->next);
    if (Kinds::unary_construction_material(K1) == NULL) {
        THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
        StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_EntryOfNonList),
            "that doesn't make sense to me as a list entry",
            "since the entry is taken from something which isn't a list.");
        return NEVER_MATCH;
    }
    if (Kinds::eq(K2, K_number) == FALSE) {
        THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
        StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_NonNumericListEntry),
            "that doesn't make sense to me as a list entry",
            "because the indication of which entry is not a number. "
            "For instance, 'entry 3 of L' is allowed, but not 'entry "
            "\"six\" of L'. (List entries are numbered 1, 2, 3, ...)");
        return NEVER_MATCH;
    }

§10.9.1.2.3. For a property value, we have to have a property and an owner (perhaps an object, perhaps a value). If the owner is a value, we need to police the availability of the property carefully, since no run-time checking can help us there.

Step (4S.d) Check arguments of a property value10.9.1.2.3 =

    LOG_DASH("(4S.d)");
    parse_node *the_property = p->down;
    kind *K1 = Specifications::to_kind(the_property);
    if (Kinds::get_construct(K1) != CON_property) Issue a "not a property" problem message10.9.1.2.3.1;
    property *prn = Rvalues::to_property(the_property);
    if (prn == NULL)
        internal_error("null property name in type checking");
    if (Properties::is_either_or(prn)) Issue a "not a value property" problem message10.9.1.2.3.2;

    parse_node *the_owner = p->down->next;
    kind *K2 = Specifications::to_kind(the_owner);
    if ((K2 == NULL) || (Specifications::is_description(the_owner)))
        Issue a problem message for being too vague about the owner10.9.1.2.3.3;

    inference_subject *owning_subject = InferenceSubjects::from_specification(the_owner);
    if (owning_subject == NULL) owning_subject = KindSubjects::from_kind(K2);
    if (PropertyPermissions::find(owning_subject, prn, TRUE) == NULL) {
        if ((Kinds::Behaviour::is_object(K2) == FALSE) ||
            ((Rvalues::is_object(the_owner)) &&
                (Rvalues::is_self_object_constant(the_owner) == FALSE)))
            Issue a problem message for not being allowed this property10.9.1.2.3.4;
    }

§10.9.1.2.3.1. Inform constructs property-value specifications quite carefully, and I think it's only possible for the typechecker to see one where the property isn't a property when recovering from other problems.

Issue a "not a property" problem message10.9.1.2.3.1 =

    THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
    Problems::quote_source(1, current_sentence);
    Problems::quote_wording(2, Node::get_text(p));
    Problems::quote_wording(3, Node::get_text(the_property));
    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(BelievedImpossible));
    Problems::issue_problem_segment(
        "In the sentence %1, it looks as if you intend '%2' to be a property "
        "of something, but there is no such property as '%3'.");
    Problems::issue_problem_end();
    return NEVER_MATCH;

§10.9.1.2.3.2. Issue a "not a value property" problem message10.9.1.2.3.2 =

    THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
    Problems::quote_source(1, current_sentence);
    Problems::quote_wording(2, Node::get_text(p));
    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_EitherOrAsValue));
    Problems::issue_problem_segment(
        "In the sentence %1, it looks as if you intend '%2' to be the value "
        "of a property of something, but that property has no value: it's "
        "something which an object either is or is not.");
    Problems::issue_problem_end();
    return NEVER_MATCH;

§10.9.1.2.3.3. Issue a problem message for being too vague about the owner10.9.1.2.3.3 =

    THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
    Problems::quote_source(1, current_sentence);
    Problems::quote_wording(2, Node::get_text(p));
    int owner_quoted = TRUE;
    if ((Specifications::to_kind(the_owner)) &&
        (Descriptions::get_quantifier(the_owner) == NULL))
        Problems::quote_kind(3, Specifications::to_kind(the_owner));
    else if (Wordings::nonempty(Node::get_text(the_owner)))
        Problems::quote_wording(3, Node::get_text(the_owner));
    else owner_quoted = FALSE;
    LOG("Owner tree is $T\n", the_owner);
    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_PropertyOfKind2));
    if (owner_quoted) {
        if (Wordings::nonempty(Node::get_text(p)))
            Problems::issue_problem_segment(
                "In the sentence %1, it looks as if you intend '%2' to be a property, "
                "but '%3' is not specific enough about who or what the owner is. ");
        else
            Problems::issue_problem_segment(
                "In the sentence %1, it looks as if you intend to look up a property "
                "of something, but '%3' is not specific enough about who or what "
                "the owner is. ");
    } else {
        if (Wordings::nonempty(Node::get_text(p)))
            Problems::issue_problem_segment(
                "In the sentence %1, it looks as if you intend '%2' to be a property, "
                "but you're not specific enough about who or what the owner is. ");
        else
            Problems::issue_problem_segment(
                "In the sentence %1, it looks as if you intend to look up a property "
                "of something, but you're not specific enough about who or what "
                "the owner is. ");
    }
    Problems::issue_problem_segment(
        "%PSometimes this mistake is made because Inform mostly doesn't understand "
        "the English language habit of referring to something indefinite by a "
        "common noun - for instance, writing 'change the carrying capacity of "
        "the container to 10' throws Inform because it doesn't understand "
        "that 'the container' means one which has been discussed recently.");
    Problems::issue_problem_end();
    return NEVER_MATCH;

§10.9.1.2.3.4. Issue a problem message for not being allowed this property10.9.1.2.3.4 =

    THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
    Problems::quote_source(1, current_sentence);
    Problems::quote_wording(2, prn->name);
    Problems::quote_subject(3, owning_subject);
    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_LookedUpForbiddenProperty));
    Problems::issue_problem_segment(
        "In the sentence %1, you seem to be looking up the '%2' property, "
        "but '%3' is not allowed to have that property. ");
    Problems::issue_problem_end();
    return NEVER_MATCH;

§10.9.1.2.4. For a table entry, we have to have a list and an index.

Step (4S.e) Check arguments of a table reference10.9.1.2.4 =

    LOG_DASH("(4S.e)");
    if (Node::no_children(p) == 4) {
        kind *col_kind = Specifications::to_kind(p->down->next);
        kind *col_contents_kind = Kinds::unary_construction_material(col_kind);
        kind *key_kind = Specifications::to_kind(p->down->next->next);
        LOGIF(MATCHING, "Kinds: col %u, contents %u, key %u\n",
            col_kind, col_contents_kind, key_kind);
        if ((Kinds::get_construct(col_kind) != CON_table_column) ||
            (col_contents_kind == NULL)) {
            THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
            StandardProblems::sentence_problem(Task::syntax_tree(), _p_(BelievedImpossible),
                "that doesn't make sense to me as a table entry",
                "since the entry is taken from something which isn't a table.");
            return NEVER_MATCH;
        }
        if ((K_snippet) &&
            (Kinds::eq(key_kind, K_snippet)) && (Kinds::eq(col_contents_kind, K_text))) {
            THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
            Problems::quote_source(1, current_sentence);
            Problems::quote_kind(2, col_contents_kind);
            Problems::quote_kind(3, key_kind);
            Problems::quote_wording(4, Node::get_text(p->down->next->next));
            StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_TableCorrFruitless2));
            Problems::issue_problem_segment(
                "In the sentence %1, you seem to be looking up a corresponding "
                "entry in a table: but you're looking up a snippet of a command "
                "(%3) in a column of text. Although those look the same, they "
                "really aren't, and no match can be made. (You might be able to "
                "fix matters by converting the snippet to text, say writing '\"[%4]\"' "
                "in place of '%4'.)");
            Problems::issue_problem_end();
            return NEVER_MATCH;
        }
        if (Kinds::compatible(key_kind, col_contents_kind) == NEVER_MATCH) {
            THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
            Problems::quote_source(1, current_sentence);
            Problems::quote_kind(2, col_contents_kind);
            Problems::quote_kind(3, key_kind);
            StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_TableCorrFruitless));
            Problems::issue_problem_segment(
                "In the sentence %1, you seem to be looking up a corresponding "
                "entry in a table: but it's fruitless to go looking for %3 "
                "in a column where each entry contains %2.");
            Problems::issue_problem_end();
            return NEVER_MATCH;
        }
    }

§10.9.1.3. Unknown found text occurs as a command10.9.1.3 =

    THIS_IS_A_GROSS_PROBLEM;
    if (<structural-phrase-problem-diagnosis>(Node::get_text(p)) == FALSE) {
        if (Wordings::mismatched_brackets(Node::get_text(p))) {
            StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_UnpairedBrackets),
                "this is a phrase which I don't recognise",
                "perhaps because it uses brackets '(' and ')' or braces '{' and '}' "
                "in a way that doesn't make sense to me. Each open '(' or '{' has "
                "to have a matching ')' or '}'.");
        } else {
            StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_UnknownPhrase),
                "this is a phrase which I don't recognise",
                "possibly because it is one you meant to define but never got round "
                "to, or because the wording is wrong (see the Phrasebook section of "
                "the Index to check). Alternatively, it may be that the text "
                "immediately previous to this was a definition whose ending, normally "
                "a full stop, is missing?");
        }
    }
    return NEVER_MATCH;

§11. "Diagnosis" nonterminals are used to parse syntax which is already known to be invalid: they simply choose between problem messages. This one picks up on misuse of structural phrases.

<structural-phrase-problem-diagnosis> ::=
    continue                                ==> Issue PM_WrongContinue problem11.1

§11.1. Issue PM_WrongContinue problem11.1 =

    StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_WrongContinue),
        "this is a phrase which I don't recognise",
        "and which isn't defined. Perhaps you wanted the phrase which "
        "would skip to the next repetition of a loop, since that's "
        "written 'continue' in some programming languages (such as C "
        "and Inform 6)? If so, what you want is 'next'.");

§12. Arithmetic operands. The following works out the kind of an operand for an arithmetic operation, which because of polymorphism is not as straightforward as it looks.

kind *Dash::fix_arithmetic_operand(parse_node *operand) {
    if (Node::is(operand->down, UNKNOWN_NT)) return NULL;
    if (Node::get_type(operand) != RVALUE_CONTEXT_NT)
        internal_error("arithmetic operand not an rvalue");
    kind *expected = NULL;
    parse_node *check = operand->down;
    if (Node::is(check, AMBIGUITY_NT)) check = check->down;
    if (Rvalues::is_CONSTANT_construction(check, CON_property)) {
        property *prn = Rvalues::to_property(check);
        if (Properties::is_either_or(prn) == FALSE)
            expected = ValueProperties::kind(prn);
    }
    kind *K = Node::get_kind_required_by_context(operand);
    Node::set_kind_required_by_context(operand, expected);
    BEGIN_DASH_MODE;
    DASH_MODE_EXIT(ISSUE_PROBLEMS_DMODE);
    DASH_MODE_CREATE(NULL);
    int rv = Dash::typecheck_recursive(operand, NULL, TRUE);
    END_DASH_MODE;
    Node::set_kind_required_by_context(operand, K);
    if (rv == NEVER_MATCH) return NULL;
    return Specifications::to_kind(operand->down);
}

§13. Local variable markers. Branches (4A) and (4I) both make use of the following code, which is applied to any invocation surviving Dash.

Here's the usual way a local variable is made. One invocation we matched is for the phrase whose prototype reads:

To let (T - nonexisting variable) be (V - value): ...

To be definite, let's suppose we are working on:

let the magic word be "Shazam [turn count] times!";

The checking code above accepted "magic word" as a new name, and marked token 0 in the invocation as one where a new variable will need to be created — which is done if and when the invocation is ever compiled.

int Dash::set_up_any_local_required(parse_node *inv) {
    for (int i=0, N = Invocations::get_no_tokens(inv); i<N; i++) {
        kind *K = Invocations::get_token_variable_kind(inv, i);
        if (K) {
            if ((i == 0) && (N >= 2) && (Kinds::eq(K, K_value)) &&
                (IDTypeData::is_a_let_assignment(Node::get_phrase_invoked(inv))))
                Infer the kind of the new variable13.1;

            int changed = FALSE;
            K = Kinds::substitute(K, NULL, &changed, FALSE);
            if (changed) LOGIF(MATCHING, "(4A.c.1) Local var amended to %u\n", K);
            Invocations::set_token_variable_kind(inv, i, K);
        }
    }
    return ALWAYS_MATCH;
}

§13.1. The following code is used to work out a good kind for the new variable, instead of "value", based on looking at token 1 — the value being assigned. In the example above, we look at this initial value,

"Shazam [turn count] times!"

and decide that K should be "text".

Infer the kind of the new variable13.1 =

    parse_node *val = Invocations::get_token_as_parsed(inv, i);
    wording W = Node::get_text(val);
    parse_node *initial_value = Invocations::get_token_as_parsed(inv, 1);
    parse_node *iv_spec = Node::get_phrase_invoked(inv)->type_data.token_sequence[1].to_match;
    if (initial_value)
        Where no kind was explicitly stated, infer this from the supplied initial value13.1.1;

§13.1.1. Unusually, it's legal for the initial value to be a kind —

let the magic digraph be a text;

This doesn't give us an initial value as such, but it explicitly tells us the kind, which is good enough.

Otherwise, we either know the kind already from polymorphism calculations, or we can work it out by seeing what the initial value evaluates to. Note that with values which are objects, we guess the kind as the broadest subkind of "object" to which the value belongs: in practice that means usually a thing, a room or a region.

We make one exception to allow lines like —

let X be a one-to-one relation of numbers to men;

where the adjective "one-to-one" forces the right hand side to be description of a relation.

Where no kind was explicitly stated, infer this from the supplied initial value13.1.1 =

    kind *seems_to_be = NULL;
    if ((Specifications::is_kind_like(iv_spec)) &&
        (Node::is(initial_value, CONSTANT_NT))) {
        kind *K = Node::get_kind_of_value(initial_value);
        if (Kinds::get_construct(K) == CON_description) {
            kind *DK = Kinds::unary_construction_material(K);
            if (Kinds::get_construct(DK) == CON_relation) seems_to_be = DK;
        }
    }
    if (seems_to_be == NULL) seems_to_be = Specifications::to_kind(initial_value);

    LOGIF(LOCAL_VARIABLES, "New variable %W from $P ($P) seems to be: %u\n",
        W, initial_value, iv_spec, seems_to_be);
    if (seems_to_be == NULL) Fail: the initial value of the local is unknown13.1.1.1;

    if ((Kinds::get_construct(seems_to_be) == CON_list_of) &&
        (Kinds::eq(Kinds::unary_construction_material(seems_to_be), K_nil)))
        Fail: the initial value of the local is the empty list13.1.1.2;
    if (Kinds::Behaviour::definite(seems_to_be) == FALSE)
        Fail: the initial value can't be stored13.1.1.3;
    LOGIF(MATCHING, "(4A.c.1) Local variable seems to have kind: %u (kind-like: %d)\n",
        seems_to_be, Specifications::is_kind_like(initial_value));

    K = seems_to_be;
    if (Specifications::is_kind_like(initial_value) == FALSE)
        if (Kinds::Behaviour::is_subkind_of_object(K))
            while (Kinds::eq(Latticework::super(K), K_object) == FALSE)
                K = Latticework::super(K);
    LOGIF(MATCHING, "(4A.c.1) Local variable inferred to have kind: %u\n", K);

§13.1.1.1. Fail: the initial value of the local is unknown13.1.1.1 =

    THIS_IS_AN_ORDINARY_PROBLEM;
    Problems::quote_source(1, current_sentence);
    Problems::quote_wording(2, Node::get_text(initial_value));
    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(BelievedImpossible));
    Problems::issue_problem_segment(
        "The phrase %1 tries to use 'let' to give a temporary name to a value, "
        "but the value ('%2') is one that I can't understand.");
    Problems::issue_problem_end();
    return NEVER_MATCH;

§13.1.1.2. Bet you didn't think of this one. Actually, the kind of the list can also collapse to just "value" if the entries are incompatible, so we call the relevant code to issue a better problem message if it can.

Fail: the initial value of the local is the empty list13.1.1.2 =

    THIS_IS_AN_ORDINARY_PROBLEM;
    int pc = problem_count;
    Lists::check_one(Node::get_text(initial_value));
    if (pc == problem_count) {
        Problems::quote_source(1, current_sentence);
        StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_CantLetEmptyList));
        Problems::issue_problem_segment(
            "The phrase %1 tries to use 'let' to give a temporary name to the "
            "empty list '{ }', but because it's empty, I can't tell what kind of "
            "value the list should have. Try 'let X be a list of numbers' (or "
            "whatever) instead.");
        Problems::issue_problem_end();
    }
    return NEVER_MATCH;

§13.1.1.3. And some kinds are just forbidden in storage:

Fail: the initial value can't be stored13.1.1.3 =

    THIS_IS_AN_ORDINARY_PROBLEM;
    StandardProblems::sentence_problem(Task::syntax_tree(), _p_(BelievedImpossible),
        "this isn't a definite kind",
        "and is instead a general description which might apply to many "
        "different kinds, so I can't see how to create this named value. "
        "(For example, 'let R be a relation' is vague because it doesn't "
        "make clear what R will relate - 'let R be a relation of numbers' "
        "would be fine.)");
    return NEVER_MATCH;

§14. Problems, problems, problems. We are now in a situation where Dash has certainly failed, and on every possible alternative reading, so it would be legitimate to return NEVER_MATCH here, which would likely result in some anodyne problem message from higher up in Dash.

But we want to produce more helpful problem messages than that. It's not entirely clear how best to do this. Often, when a node fails, it fails for seven different reasons — each different possibility fails for a different cause. We want, somehow, to guess which was the most likely to have been intended and to report the problem with that one.

int Dash::failed_one(parse_node *inv, parse_node *context, kind *kind_needed) {
    parse_node *list[1];
    list[0] = inv;
    return Dash::failed(list, 1, context, kind_needed);
}

wording PM_BadIntermediateKind_wording = EMPTY_WORDING_INIT;

int Dash::failed(parse_node **list_of_possible_readings, int no_of_possible_readings,
    parse_node *context, kind *kind_needed) {
    if (Dash::problems_have_been_issued()) return NEVER_MATCH;

    parse_node *first_inv_in_group = NULL;
    parse_node *first_failing_interestingly = NULL;
    parse_node *first_not_failing_grossly = NULL;
    wording SW = EMPTY_WORDING;
    int list_includes_lets = FALSE;
    int nongross_count = 0;
    Scan through the invocations in the problematic group, gathering information14.1;

    parse_node *most_likely_to_have_been_intended = NULL;
    Decide which invocation is the one most likely to have been intended14.2;

    int pc_before = problem_count;
    if (first_failing_interestingly)
        Re-type-check the first interesting invocation, allowing interesting problems this time14.3
    else if (most_likely_to_have_been_intended)
        Re-type-check the tokens of the most likely invocation with silence off14.4
    if (problem_count == pc_before) {
        if (Wordings::nonempty(SW)) <failed-text-substitution-diagnosis>(SW);
        else Issue a problem for a regular phrase with multiple failed possibilities14.5;
    }
    return NEVER_MATCH;
}

§14.1. Scan through the invocations in the problematic group, gathering information14.1 =

    for (int ref=0; ref<no_of_possible_readings; ref++) {
        parse_node *inv = list_of_possible_readings[ref];
        if ((Dash::test_flag(inv, INTERESTINGLY_FAILED_DASHFLAG)) &&
            (first_failing_interestingly == NULL)) first_failing_interestingly = inv;
        if (first_inv_in_group == NULL) first_inv_in_group = inv;
        id_body *idb = Node::get_phrase_invoked(inv);
        if (idb) {
            if (IDTypeData::is_a_let_assignment(idb)) list_includes_lets = TRUE;
            if (Dash::test_flag(inv, GROSSLY_FAILED_DASHFLAG) == FALSE) {
                first_not_failing_grossly = inv;
                if (IDTypeData::is_a_say_X_phrase(&(idb->type_data))) SW = Node::get_text(inv);
                nongross_count++;
            }
        }
    }

§14.2. Decide which invocation is the one most likely to have been intended14.2 =

    if (first_failing_interestingly)
        most_likely_to_have_been_intended = first_failing_interestingly;
    else if ((nongross_count > 1) && (list_includes_lets))
        most_likely_to_have_been_intended = first_not_failing_grossly;
    else if (nongross_count == 1)
        most_likely_to_have_been_intended = first_not_failing_grossly;
    else if ((nongross_count == 0) && (first_inv_in_group))
        most_likely_to_have_been_intended = first_inv_in_group;

§14.3. Re-type-check the first interesting invocation, allowing interesting problems this time14.3 =

    BEGIN_DASH_MODE;
    DASH_MODE_ENTER(ISSUE_INTERESTING_PROBLEMS_DMODE);
    DASH_MODE_CREATE(NULL);
    Dash::typecheck_recursive(first_failing_interestingly, context, FALSE);
    END_DASH_MODE;

§14.4. Re-type-check the tokens of the most likely invocation with silence off14.4 =

    int ec = problem_count;
    BEGIN_DASH_MODE;
    DASH_MODE_ENTER(ISSUE_PROBLEMS_DMODE);
    DASH_MODE_CREATE(NULL);
    for (int i=0; i<Invocations::get_no_tokens(most_likely_to_have_been_intended); i++) {
        Dash::typecheck_recursive(Invocations::get_token(most_likely_to_have_been_intended, i), context, TRUE);
        if (problem_count > ec) break;
    }
    if (problem_count == ec) {
        LOGIF(MATCHING, "Try again in local problems mode\n");
        BEGIN_DASH_MODE;
        DASH_MODE_ENTER(ISSUE_LOCAL_PROBLEMS_DMODE);
        DASH_MODE_CREATE(NULL);
        for (int i=0; i<Invocations::get_no_tokens(most_likely_to_have_been_intended); i++) {
            Dash::typecheck_recursive(Invocations::get_token(most_likely_to_have_been_intended, i), context, TRUE);
            if (problem_count > ec) break;
        }
        END_DASH_MODE;
    }
    END_DASH_MODE;
    if (problem_count == ec) {
        kind *K = Node::get_kind_resulting(most_likely_to_have_been_intended);
        kind *W = kind_needed;
        if ((K) && (W) && (Kinds::compatible(K, W) == NEVER_MATCH)) {
            THIS_IS_AN_ORDINARY_PROBLEM;
            wording PW = Node::get_text(list_of_possible_readings[0]);
            if (!(Wordings::eq(PM_BadIntermediateKind_wording, PW))) {
                PM_BadIntermediateKind_wording = PW;
                Problems::quote_source(1, current_sentence);
                Problems::quote_wording(2, PW);
                Problems::quote_kind(3, K);
                Problems::quote_kind(4, W);
                StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_BadIntermediateKind));
                Problems::issue_problem_segment(
                    "In %1, the phrase '%2' doesn't seem to fit: I was hoping it would "
                    "be %4, but in fact it's %3.");
                Problems::issue_problem_end();
            }
            return NEVER_MATCH;
        }
        if (problem_count == ec) {
            LOGIF(MATCHING, "Try again in gross problems mode\n$T\n", most_likely_to_have_been_intended);
            BEGIN_DASH_MODE;
            DASH_MODE_ENTER(ISSUE_GROSS_PROBLEMS_DMODE);
            DASH_MODE_CREATE(NULL);
            Dash::typecheck_recursive(most_likely_to_have_been_intended, context, FALSE);
            END_DASH_MODE;
        }
        if (problem_count == 0)
            StandardProblems::sentence_problem(Task::syntax_tree(), _p_(BelievedImpossible),
                "the ingredients in this phrase do not fit it",
                "and I am confused enough by this that I can't give a very helpful "
                "problem message. Sorry about that.");
    }

§14.5. Issue a problem for a regular phrase with multiple failed possibilities14.5 =

    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_AllInvsFailed));
    Problems::quote_source(1, current_sentence);
    Problems::issue_problem_segment(
        "You wrote %1, which I tried to match against several possible phrase "
        "definitions. None of them worked.");
    Problems::issue_problem_end();

§15. The following chooses a problem message for a text substitution which is unrecognised.

<failed-text-substitution-diagnosis> ::=
    a list of ... |    ==> Issue PM_SayAList problem15.1
    ...                                 ==> Issue last-resort failed ts problem15.2

§15.1. Issue PM_SayAList problem15.1 =

    StandardProblems::sentence_in_detail_problem(Task::syntax_tree(), _p_(PM_SayAList), W,
        "this asked to say 'a list of...'",
        "which I read as being a general description applying to some "
        "lists and not others, so it's not something which can be said. "
        "(Maybe you meant 'the list of...' instead? That normally makes "
        "a definite list of whatever matches the '...' part.)");

§15.2. Issue last-resort failed ts problem15.2 =

    StandardProblems::sentence_in_detail_problem(Task::syntax_tree(), _p_(BelievedImpossible), W,
        "this asked to say something which I do not recognise",
        "either as a value or as one of the possible text substitutions.");

§16. In the final checklist of doomed possibility, the code to quote an invocation in a problem message will call the following routine for each parsed token. This remembers the token so that it can be explained in notes at the end of the big list; but each word range is remembered only once, for brevity. We don't gloss the meanings of literal constants like 26 or "frog" since these are glaringly obvious.

void Dash::note_inv_token_text(parse_node *p, int new_name) {
    inv_token_problem_token *itpt;
    LOOP_OVER(itpt, inv_token_problem_token)
        if (Wordings::eq(itpt->problematic_text, Node::get_text(p))) {
            if (new_name) itpt->new_name = TRUE;
            return;
        }
    itpt = CREATE(inv_token_problem_token);
    itpt->problematic_text = Node::get_text(p);
    itpt->new_name = new_name;
    if (Node::is(p, AMBIGUITY_NT)) p = p->down;
    itpt->as_parsed = p; itpt->already_described = FALSE;
    if ((Rvalues::is_CONSTANT_of_kind(p, K_number)) ||
        (Rvalues::is_CONSTANT_of_kind(p, K_text))) itpt->already_described = TRUE;
}

§17. This last little grammar diagnoses problems with a condition, and helps to construct a problem message which will (usually) show which part of a compound condition caused the trouble:

<condition-problem-diagnosis> ::=
    <condition-problem-part> <condition-problem-part-tail> |  ==> { R[1] | R[2], - }
    <condition-problem-part>                                  ==> { pass 1 }

<condition-problem-part-tail> ::=
    , and/or <condition-problem-diagnosis> |                  ==> { pass 1 }
    ,/and/or <condition-problem-diagnosis>                    ==> { pass 1 }

<condition-problem-part> ::=
    <s-condition> |    ==> { 0, - }; Quote this-condition-okay segment17.1;
    <s-value> |        ==> { INVALID_CP_BIT, - }; Quote this-condition-value segment17.2;
    ... begins/ends |  ==> { WHENWHILE_CP_BIT+INVALID_CP_BIT, - }; Quote scene-begins-or-ends segment17.3;
    when/while *** |   ==> { WHENWHILE_CP_BIT+INVALID_CP_BIT, - }; Quote this-condition-bad segment17.4;
    ...                ==> { INVALID_CP_BIT, - }; Quote this-condition-bad segment17.4;

§17.1. Quote this-condition-okay segment17.1 =

    if (preform_lookahead_mode == FALSE) {
        Problems::quote_wording(4, W);
        Problems::issue_problem_segment("'%4' was okay; ");
    }

§17.2. Quote this-condition-value segment17.2 =

    if (preform_lookahead_mode == FALSE) {
        Problems::quote_wording(4, W);
        Problems::issue_problem_segment(
            "'%4' only made sense as a value, which can't be used as a condition; ");
    }

§17.3. Quote scene-begins-or-ends segment17.3 =

    if (preform_lookahead_mode == FALSE) {
        Problems::quote_wording(4, W);
        Problems::issue_problem_segment(
            "'%4' did not make sense as a condition, but looked as if it might "
            "be a way to specify a beginning or end for a scene - but such things "
            "can't be divided by 'or'; ");
    }

§17.4. Quote this-condition-bad segment17.4 =

    if (preform_lookahead_mode == FALSE) {
        Problems::quote_wording(4, W);
        Problems::issue_problem_segment("'%4' did not make sense; ");
    }

§18. (5) Single nodes. Here we typecheck a single non-invocation node on its own terms, ignoring any children it may have.

int Dash::typecheck_single_node(parse_node *p, kind *kind_expected, int condition_context) {
    LOG_DASH("(5)");
    LOGIF(MATCHING, "Kind expected: %u, condition expected: %d\n", kind_expected, condition_context);
    int outcome = ALWAYS_MATCH;  drops to SOMETIMES_MATCH if a need for run-time checking is realised

    if ((Rvalues::is_nothing_object_constant(p)) &&
        (kind_expected) && (Kinds::Behaviour::is_subkind_of_object(kind_expected)))
        Disallow "nothing" as a match for a description requiring a kind of object18.1;

    Step (5.a) Deal with the UNKNOWN_NT18.2;
    Step (5.b) Deal with bare property names18.3;
    Step (5.c) Deal with any attached proposition18.4;
    Step (5.d) Apply miscellaneous other coercions18.5;
    Step (5.e) The Main Rule of Type-Checking18.6;
    return outcome;
}

§18.1. "You can't have/ Something for nothing," as Canadian power-trio Rush tell us with the air of having just made a great discovery; well, you can't have "nothing" for something, either —

Disallow "nothing" as a match for a description requiring a kind of object18.1 =

    THIS_IS_AN_ORDINARY_PROBLEM;
    Problems::quote_source(1, current_sentence);
    Problems::quote_wording(2, Node::get_text(p));
    Problems::quote_kind(3, kind_expected);
    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_NothingForSomething));
    Problems::issue_problem_segment(
        "You wrote %1, but '%2' is literally no thing, and it consequently does "
        "not count as %3.");
    Problems::issue_problem_end();
    return NEVER_MATCH;

§18.2. Rule (5.a). In all cases, unknown text in found is incorrect. We can produce any of more than twenty different problem messages here, in an attempt to be helpful about what exactly is wrong.

define SAY_UTSHAPE 1
define LIST_UTSHAPE 2
define NO_UTSHAPE 3

Step (5.a) Deal with the UNKNOWN_NT18.2 =

    LOG_DASH("(5.a)");
    if (Node::is(p, UNKNOWN_NT)) {
        THIS_IS_A_GROSS_PROBLEM;
        LOG("(5.a) problem message:\nfound: $Texpected: %u", p, kind_expected);
        #ifdef IF_MODULE
        if (Kinds::eq(kind_expected, K_stored_action))
            Unknown found text occurs as an action to try18.2.1;
        #endif
        Problems::quote_source_eliding_begin(1, current_sentence);
        Problems::quote_wording(2, Node::get_text(p));
        if (condition_context)
            Problems::quote_text(3, "a condition");
        else if (kind_expected == NULL)
            Problems::quote_kind(3, K_value);
        else
            Problems::quote_kind(3, kind_expected);

        int shape = NO_UTSHAPE;
        if (current_sentence) {
            <unknown-text-shape>(Node::get_text(current_sentence));
            shape = <<r>>;
        }
        if (shape == NO_UTSHAPE) {
            <unknown-text-shape>(Node::get_text(p));
            shape = <<r>>;
        }

        int preceding = Wordings::first_wn(Node::get_text(p)) - 1;
        if ((preceding >= 0) && (current_sentence) &&
            (((TextSubstitutions::currently_compiling()) || (shape == SAY_UTSHAPE))) &&
            ((preceding == Wordings::first_wn(Node::get_text(current_sentence)))
                || (Lexer::word(preceding) == COMMA_V)))
            Unknown found text occurs as a text substitution18.2.2
        else if ((condition_context) && (shape == LIST_UTSHAPE))
            Issue a problem message for a compound condition which has gone bad18.2.3
        else
            Issue a problem message for miscellaneous suspicious wordings18.2.4;

        return NEVER_MATCH;
    }

§18.2.1. Unknown found text occurs as an action to try18.2.1 =

    parse_node *spec = NULL;
    kind *K = NULL, *K2 = NULL;
    Dash::clear_validation_case();
    <action-pattern>(Node::get_text(p));
    if (Dash::get_validation_case(&spec, &K, &K2)) {
        StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_UnknownTryAction1));
        Problems::quote_source(1, current_sentence);
        Problems::quote_wording(2, Node::get_text(p));
        Problems::quote_wording(3, Node::get_text(spec));
        Problems::quote_kind(4, K);
        Problems::quote_kind(5, K2);
        Problems::issue_problem_segment(
            "You wrote %1, but '%2' is not an action I can try. This looks as "
            "if it might be because it contains something of the wrong kind. "
            "My best try involved seeing if '%3' could be %4, which might have "
            "made sense, but it turned out to be %5.");
        Problems::issue_problem_end();
    } else {
        StandardProblems::sentence_problem(Task::syntax_tree(), _p_(PM_UnknownTryAction2),
            "this is not an action I recognise",
            "or else is malformed in a way I can't see how to sort out.");
    }
    return NEVER_MATCH;

§19. The <unknown-text-shape> is used purely in diagnosing problems; it helps to decide, for instance, whether the errant phrase was intended to be a text substitution or not.

<unknown-text-shape> ::=
    say ... |         ==> { SAY_UTSHAPE, - }
    ... and/or ... |  ==> { LIST_UTSHAPE, - }
    ...               ==> { NO_UTSHAPE, - }

<unknown-text-substitution-problem-diagnosis> ::=
    , ... |    ==> Issue PM_SayComma problem19.1
    unicode ... |    ==> Issue PM_SayUnicode problem19.2
    ... condition |    ==> Issue PM_SayUnknownCondition problem19.4
    otherwise/else *** |    ==> Issue PM_SayElseMisplaced problem19.3
    ...                 ==> Issue PM_SayUnknown problem19.5

§19.1. Issue PM_SayComma problem19.1 =

    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_SayComma));
    Problems::issue_problem_segment(
        "In the line %1, I was expecting that '%2' would be something to "
        "'say', but unexpectedly it began with a comma. The usual form is "
        "just 'say \"text\"', perhaps with some substitutions in square "
        "brackets within the quoted text, but no commas.");
    Problems::issue_problem_end();

§19.2. Issue PM_SayUnicode problem19.2 =

    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_SayUnicode));
    Problems::issue_problem_segment(
        "In the line %1, I was expecting that '%2' would be something to "
        "'say', but it didn't look like any form of 'say' that I know. "
        "So I tried to read '%2' as a Unicode character, which seemed "
        "likely because of the word 'unicode', but that didn't work either. "
        "%PUnicode characters can be written either using their decimal "
        "numbers - for instance, 'Unicode 2041' - or with their standard "
        "names - 'Unicode Latin small ligature oe'. For efficiency reasons "
        "these names are only available if you ask for them; to make them "
        "available, you need to 'Include Unicode Character Names by Graham "
        "Nelson' or, if you really need more, 'Include Unicode Full "
        "Character Names by Graham Nelson'.");
    Problems::issue_problem_end();

§19.3. Issue PM_SayElseMisplaced problem19.3 =

    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_SayElseMisplaced));
    Problems::issue_problem_segment(
        "In the line %1, I was expecting that '%2' would be something to "
        "'say', but unexpectedly I found an 'otherwise' (or 'else'). That "
        "would be fine inside an '[if ...]' part of the text, but doesn't "
        "make sense on its own.");
    Problems::issue_problem_end();

§19.4. Issue PM_SayUnknownCondition problem19.4 =

    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_SayUnknownCondition));
    Problems::issue_problem_segment(
        "In the line %1, I was expecting that '%2' would be something to "
        "'say', but it didn't look like any form of 'say' that I know. So "
        "I tried to read '%2' as a value of some kind (because it's legal "
        "to say values), but couldn't make sense of it that way either. "
        "%PSometimes this happens because punctuation has gone wrong - "
        "for instance, if you've omitted a semicolon or full stop at the "
        "end of the 'say' phrase.");
    Problems::issue_problem_segment(
        "%PNames which end in 'condition' often represent the current "
        "state of something which can be in any one of three or more "
        "states. This will only be the case if you have explicitly said "
        "so, with a line like 'The rocket is either dry, fuelled or launched.' - "
        "in which case the value 'rocket condition' will always be one "
        "of 'dry', 'fuelled' or 'launched'. Note that all of this only "
        "applies to a list of three or more possibilities - a thing can "
        "have any number of either/or properties. For instance, a "
        "container is open or closed, but it also transparent or opaque. "
        "Neither of these counts as its 'condition'.");
    Problems::issue_problem_end();

§19.5. Issue PM_SayUnknown problem19.5 =

    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_SayUnknown));
    Problems::issue_problem_segment(
        "In the line %1, I was expecting that '%2' would be something to "
        "'say', but it didn't look like any form of 'say' that I know. So "
        "I tried to read '%2' as a value of some kind (because it's legal "
        "to say values), but couldn't make sense of it that way either. "
        "%PSometimes this happens because punctuation has gone wrong - "
        "for instance, if you've omitted a semicolon or full stop at the "
        "end of the 'say' phrase.");
    Problems::issue_problem_end();

§18.2.2. Unknown found text occurs as a text substitution18.2.2 =

    <unknown-text-substitution-problem-diagnosis>(Node::get_text(p));

§18.2.3. It's a bit unenlightening when an entire condition is rejected as unknown if, in fact, only one of perhaps many clauses is broken. We therefore produce quite an elaborate problem message which goes through the clauses, summing up their status in turn:

define INVALID_CP_BIT 1
define WHENWHILE_CP_BIT 2

Issue a problem message for a compound condition which has gone bad18.2.3 =

    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_CompoundConditionFailed));
    Problems::issue_problem_segment(
        "In the sentence %1, I was expecting that '%2' would be a condition. "
        "It didn't make sense as one long phrase, but because it was divided up by "
        "'and'/'or', I tried breaking it down into smaller conditions, but "
        "that didn't work either. ");
    <condition-problem-diagnosis>(Node::get_text(p));
    int dubious = <<r>>;
    if (dubious & INVALID_CP_BIT)
        Problems::issue_problem_segment(
            "so I ran out of ideas.");
    else
        Problems::issue_problem_segment(
            "but that combination of conditions isn't allowed to be joined "
            "together with 'and' or 'or', because that would just be too confusing. "
            "%PFor example, 'if the player is carrying a container or a "
            "supporter' has an obvious meaning in English, but Inform reads "
            "it as two different conditions glued together: 'if the player is "
            "carrying a container', and also 'a supporter'. The meaning of "
            "the first is obvious. The second part is true if the current "
            "item under discussion is a supporter - for instance, the noun of "
            "the current action, or the item to which a definition applies. "
            "Both of these conditions are useful in different circumstances, "
            "but combining them in one condition like this makes a very "
            "misleading line of text. So Inform disallows it.");
    if (dubious & WHENWHILE_CP_BIT)
        Problems::issue_problem_segment(
            "%PI notice there's a 'when' or 'while' being used as the opening "
            "word of one of those conditions, though; maybe that's the problem?");
    Problems::issue_problem_end();

§20. These are cases where the wording used in the source text suggests some common misunderstanding.

<unknown-value-problem-diagnosis> ::=
    turns |    ==> Issue PM_NumberOfTurns problem20.1
    ... is/are out of play |    ==> Issue PM_OutOfPlay problem20.2
    unicode ... |    ==> Issue PM_MidTextUnicode problem20.5
    ... condition |    ==> Issue PM_UnknownCondition problem20.6
    ...                             ==> Issue PM_Unknown problem20.8

<unknown-use-option-diagnosis> ::=
    ... ^option |    ==> Issue PM_OptionlessOption problem20.3
    ...                             ==> Issue PM_Unknown problem20.8

<unknown-activity-diagnosis> ::=
    ... of |    ==> Issue PM_ActivityOf problem20.4
    ... for |    ==> Issue PM_ActivityWithFor problem20.7
    ...                             ==> Issue PM_Unknown problem20.8

§20.1. Issue PM_NumberOfTurns problem20.1 =

    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_NumberOfTurns));
    Issue the generic unknown wording message20.1.1;
    Problems::issue_problem_segment(
        "%PPerhaps by 'turns' you meant the number of turns of play to date? "
        "If so, try 'turn count' instead.");
    Problems::issue_problem_end();

§20.2. Issue PM_OutOfPlay problem20.2 =

    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_OutOfPlay));
    Issue the generic unknown wording message20.1.1;
    Problems::issue_problem_segment(
        "%PPeople sometimes say that things or people removed from all "
        "rooms are 'out of play', but Inform uses the adjective "
        "'off-stage' - for instance, 'if the ball is off-stage'. "
        "If you would like 'out of play' to work, you could always "
        "write 'Definition: A thing is out of play if it is off-stage.' "
        "Then the two would be equivalent.");
    Problems::issue_problem_end();

§20.3. Issue PM_OptionlessOption problem20.3 =

    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_OptionlessOption));
    Issue the generic unknown wording message20.1.1;
    Problems::issue_problem_segment(
        "%PThe names of use options, on the rare occasions when they "
        "appear as values, always end with the word 'option' - for "
        "instance, we have to write 'American dialect option' not "
        "'American dialect'. As your text here doesn't end with the "
        "word 'option', perhaps you've forgotten this arcane rule?");
    Problems::issue_problem_end();

§20.4. Issue PM_ActivityOf problem20.4 =

    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_ActivityOf));
    Issue the generic unknown wording message20.1.1;
    Problems::issue_problem_segment(
        "%PActivity names rarely end with 'of': for instance, when we talk "
        "about 'printing the name of something', properly speaking "
        "the activity is called 'printing the name'. Maybe that's it?");
    Problems::issue_problem_end();

§20.5. Issue PM_MidTextUnicode problem20.5 =

    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_MidTextUnicode));
    Issue the generic unknown wording message20.1.1;
    Problems::issue_problem_segment(
        "%PMaybe you intended this to produce a Unicode character? "
        "Unicode characters can be written either using their decimal "
        "numbers - for instance, 'Unicode 2041' - or with their standard "
        "names - 'Unicode Latin small ligature oe'. For the full list of "
        "those names, see the Unicode standard version 15.0.0.");
    Problems::issue_problem_end();

§20.6. Issue PM_UnknownCondition problem20.6 =

    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_UnknownCondition));
    Issue the generic unknown wording message20.1.1;
    Problems::issue_problem_segment(
        "%PNames which end in 'condition' often represent the current "
        "state of something which can be in any one of three or more "
        "states. Names like this only work if you've declared them, with "
        "a line like 'The rocket is either dry, fuelled or launched.' - "
        "in which case the value 'rocket condition' will always be one "
        "of 'dry', 'fuelled' or 'launched'. Maybe you forgot to declare "
        "something like this, or mis-spelled the name of the owner?");
    Problems::issue_problem_end();

§20.7. Issue PM_ActivityWithFor problem20.7 =

    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_ActivityWithFor));
    Issue the generic unknown wording message20.1.1;
    Problems::issue_problem_segment(
        "%PWere you by any chance meaning to refer to an activity by name, "
        "and used the word 'for' at the end of that name? If so, try removing "
        "just the word 'for'.");
    Problems::issue_problem_end();

§20.8. Issue PM_Unknown problem20.8 =

    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_Unknown));
    Issue the generic unknown wording message20.1.1;
    Problems::issue_problem_end();

§20.1.1. Issue the generic unknown wording message20.1.1 =

    Problems::issue_problem_segment(
        "In the sentence %1, I was expecting to read %3, but instead found some "
        "text that I couldn't understand - '%2'. ");

§18.2.4. Issue a problem message for miscellaneous suspicious wordings18.2.4 =

    if (Kinds::eq(kind_expected, K_use_option)) {
        <unknown-use-option-diagnosis>(Node::get_text(p));
    } else if (Kinds::get_construct(kind_expected) == CON_activity) {
        <unknown-activity-diagnosis>(Node::get_text(p));
    } else {
        <unknown-value-problem-diagnosis>(Node::get_text(p));
    }

§18.3. Rule (5.b). This is all concerned with a shorthand far more convenient to an Inform author than it is to us — where a property's name is used without any indication of its owner.

Step (5.b) Deal with bare property names18.3 =

    LOG_DASH("(5.b)");
    if (Kinds::get_construct(kind_expected) != CON_property) {
        parse_node *check = p;
        if (Node::is(check, AMBIGUITY_NT)) check = check->down;
        if (Rvalues::is_CONSTANT_construction(check, CON_property)) {
            property *prn = Rvalues::to_property(check);
            Step (5.b.2) If a bare property name is used where we expect a value, coerce it if the kinds allow18.3.1;
        }
    }

§18.3.1. But more often we want a value which just happens in this case to come from a property. For instance, in a text routine printing a description like "The cedarwood box could hold [carrying capacity in words] item[s].", we want "carrying capacity" to be a number value, and we treat it as if it read "carrying capacity of the cedarwood box".

We don't coerce if the property holds a relation, because letting a variable be a description of a relation tries to create a local relation on the stack frame, and this is unlikely to be what anyone wanted.

Step (5.b.2) If a bare property name is used where we expect a value, coerce it if the kinds allow18.3.1 =

    LOG_DASH("(5.b.2)");
    if (kind_expected) {
    LOG_DASH("(5.b.2a)");
        if (Properties::is_value_property(prn)) {
    LOG_DASH("(5.b.2b)");
            kind *kind_if_coerced = ValueProperties::kind(prn);
            int verdict = Kinds::compatible(kind_if_coerced, kind_expected);
            if (verdict != NEVER_MATCH) {
    LOG_DASH("(5.b.2c)");
                Coerce into a property of the "self" object18.3.1.1;
                return verdict;
            }
            if ((Kinds::get_construct(kind_expected) == CON_description) &&
                (Kinds::get_construct(kind_if_coerced) != CON_relation)) {
                LOGIF(MATCHING, "(5.b.2) coercing to description\n");
                parse_node *become = Specifications::from_kind(kind_if_coerced);
                Node::set_text(become, Node::get_text(p));
                Node::copy_in_place(p, Descriptions::to_rvalue(become));
                p->down = NULL;
            } else {
                LOGIF(MATCHING, "(5.b.2) declining to cast into property value form\n");
                return verdict;
            }
        }
    }

§18.3.1.1. The tricky part is working out what the implicitly meant object is, a classic donkey anaphora-style problem in linguistics. We don't even begin to solve that here: indeed the decision is taken rather indirectly, because we simply compile code which uses Inform 6's self variable to refer to the owner. The I6 library and our own run-time code conspire to ensure that self is always equal to something sensible.

Coerce into a property of the "self" object18.3.1.1 =

    parse_node *was = p->next_alternative;
    parse_node *pr = Node::duplicate(p);
    pr->next_alternative = NULL;
    parse_node *become = Lvalues::new_PROPERTY_VALUE(pr, Rvalues::new_self_object_constant());
    Node::copy(p, become);
    p->next_alternative = was;

    LOGIF(MATCHING, "(5.b) coercing PROPERTY to PROPERTY VALUE: $P\n", p);

§18.4. Rule (5.c). An unchecked SP can contain a proposition which, though valid as a predicate calculus sentence, makes no sense for type reasons: for instance, "the Orange Room is 10" compiles to a valid sentence but one in which the binary predicate for equality is applied to incomparable SPs. To type-check, we must prove that any proposition needed is valid on these grounds, and we delegate that to "Type Check Propositions.w".

Step (5.c) Deal with any attached proposition18.4 =

    LOG_DASH("(5.c)");
    char *desired_to = NULL;
    if (Node::is(p, TEST_PROPOSITION_NT)) desired_to = "be a condition";
    if (Descriptions::is_complex(p)) desired_to = "be a description";

    if (desired_to) {
        if (TypecheckPropositions::type_check(Specifications::to_proposition(p),
            TypecheckPropositions::tc_no_problem_reporting())
            == NEVER_MATCH) {
            LOGIF(MATCHING, "(5.c) on $P failed proposition type-checking: $D\n",
                p, Specifications::to_proposition(p));
            THIS_IS_A_GROSS_PROBLEM;
            TypecheckPropositions::type_check(Specifications::to_proposition(p),
                TypecheckPropositions::tc_problem_reporting(Node::get_text(p), desired_to));
            return NEVER_MATCH;
        } else { LOG_DASH("(5.c) Okay!"); }
    }

§18.5. Rule (5.d). Something of a grab-bag, this one. What these three situations have in common is that all use the typechecker to clarify ambiguities in syntax.

Step (5.d) Apply miscellaneous other coercions18.5 =

    #ifdef IF_MODULE
    Step (5.d.1) Coerce TEST ACTION to constant action18.5.1;
    #endif
    Step (5.d.2) Coerce constant TEXT and TEXT ROUTINE to UNDERSTANDING18.5.2;
    Step (5.d.3) Coerce a description to a value, if we expect a noun-like description18.5.3;
    Step (5.d.4) Reject plausible but wrong uses due to use of inline-only types in phrases18.5.4;

§18.5.1. An action pattern can be an action if specific enough, and this is crucial: it enables phrases such as "try taking the box" to work. When such phrases are type-checked, they expect the argument to be a constant action value, which is a specific action.

Step (5.d.1) Coerce TEST ACTION to constant action18.5.1 =

    LOG_DASH("(5.d.1)");
    if ((AConditions::is_action_TEST_VALUE(p)) && (kind_expected) &&
        (Kinds::compatible(K_stored_action, kind_expected))) {
        explicit_action *ea = Node::get_constant_explicit_action(p->down);
        if (ea == NULL) {
            action_pattern *ap = Node::get_constant_action_pattern(p->down);
            int failure_code = 0;
            ea = ExplicitActions::from_action_pattern(ap, &failure_code);

            if (failure_code == UNDERSPECIFIC_EA_FAILURE) {
                THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
                Problems::quote_source(1, current_sentence);
                Problems::quote_wording(2, Node::get_text(p));
                StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_ActionNotSpecific));
                Problems::issue_problem_segment(
                    "You wrote %1, but '%2' is too vague to describe a specific action. "
                    "%PIt has to be an exact instruction about what is being done, and "
                    "to what. For instance, 'taking the box' is fine, but 'dropping or "
                    "taking something openable' is not.");
                Problems::issue_problem_end();
                return NEVER_MATCH;
            }
            if (failure_code == OVERSPECIFIC_EA_FAILURE) {
                THIS_IS_A_GROSSER_THAN_GROSS_PROBLEM;
                Problems::quote_source(1, current_sentence);
                Problems::quote_wording(2, Node::get_text(p));
                StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_ActionTooSpecific));
                Problems::issue_problem_segment(
                    "You wrote %1, but '%2' imposes too many restrictions on the "
                    "action to be carried out, by saying something about the "
                    "circumstances which you can't guarantee will be true. "
                    "%PSometimes this problem appears because I've misread text like "
                    "'in ...' as a clause saying that the action takes place in a "
                    "particular room, when in fact it was part of the name of one of "
                    "the items involved. If that's the problem, try using 'let' to "
                    "create a simpler name for it, and then rewrite the 'try' to use "
                    "that simpler name - the ambiguity should then vanish.");
                Problems::issue_problem_end();
                return NEVER_MATCH;
            }
        }
        Node::copy_in_place(p, p->down);
        p->down = NULL;
        Node::set_kind_of_value(p, K_stored_action);
        Node::set_constant_explicit_action(p, ea);
        Node::set_constant_action_pattern(p, NULL);
        LOGIF(MATCHING, "Coerced to sa: $P\n", p);
        return ALWAYS_MATCH;
    }
    if ((condition_context) &&
        (Node::is(p, CONSTANT_NT))) {
        kind *E = Specifications::to_kind(p);
        if (Kinds::compatible(E, K_stored_action)) {
            parse_node *val = Node::duplicate(p);
            Node::copy_in_place(p, Node::new(TEST_VALUE_NT));
            Node::set_text(p, Node::get_text(val));
            p->down = val;
            LOGIF(MATCHING, "Coerced back again to sa: $P\n", p);
            return ALWAYS_MATCH;
        }
    }

§18.5.2. The following applies only to literal text in double-quotes, which might or might not include text substitutions in square brackets: if we check it against "understanding", then we are trying to interpret it as a grammar to parse rather than text to print. We need to coerce since these have very different representations at run-time.

Step (5.d.2) Coerce constant TEXT and TEXT ROUTINE to UNDERSTANDING18.5.2 =

    LOG_DASH("(5.d.2)");
    if ((Rvalues::is_CONSTANT_of_kind(p, K_text)) &&
        (K_understanding) && (Kinds::eq(kind_expected, K_understanding))) {
        Node::set_kind_of_value(p, K_understanding);
    }

§18.5.3. Another ambiguity is that the text "women who are in lighted rooms" in:

let N be the number of women who are in lighted rooms;

...is parsed as a description, a condition. But in fact it's a noun here — it has to be a value, in fact, which can go into the "number of..." phrase as an argument. We make this happen by coercing it to a constant value, using the "description of..." constructor.

Step (5.d.3) Coerce a description to a value, if we expect a noun-like description18.5.3 =

    LOG_DASH("(5.d.3)");
    kind *domain = NULL;
    if (Kinds::get_construct(kind_expected) == CON_description) {
        domain = Kinds::unary_construction_material(kind_expected);
    }
    if ((domain) && (Specifications::is_description(p))) {
        LOGIF(MATCHING, "(5.d.3) requiring description of %u\n", domain);
        kind *K = Specifications::to_kind(p);
        if (K == NULL) K = K_object;
        LOGIF(MATCHING, "(5.d.3) finding description of %u\n", K);
        int made_match = TRUE;
        if (Kinds::compatible(K, domain) == NEVER_MATCH) made_match = FALSE;
        Throw out the wrong sort of description with a seldom-seen problem message18.5.3.2;
        quantifier *q = Descriptions::get_quantifier(p);
        if ((q) && (q != not_exists_quantifier) && (q != for_all_quantifier))
            Issue a problem message for a quantified proposition in the description18.5.3.1;
        parse_node *as_con = Descriptions::to_rvalue(p);
        if (as_con == NULL)
            Issue a problem message for a malformed proposition in the description18.5.3.3;
        Node::copy_in_place(p, as_con);
        p->down = NULL;
        return ALWAYS_MATCH;
    }

§18.5.3.1. This is for undescriptive descriptions, really.

Issue a problem message for a quantified proposition in the description18.5.3.1 =

    THIS_IS_AN_INTERESTING_PROBLEM {
        Problems::quote_source(1, current_sentence);
        Problems::quote_wording(2, Node::get_text(p));
        StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_BadQuantifierInDescription));
        Problems::issue_problem_segment(
            "In %1 you wrote the description '%2' in the context of a value, "
            "but descriptions used that way are not allowed to talk about "
            "quantities. For example, it's okay to write 'an even number' "
            "as a description value, but not 'three numbers' or 'most numbers'.");
        Problems::issue_problem_end();
    }
    return NEVER_MATCH;

§18.5.3.2. The following message is seldom seen since most phrases using descriptions are set up with two parallel versions. As every description matches exactly one of these, there won't be a problem. But just in case the user has intentionally defined a phrase for only one case:

Throw out the wrong sort of description with a seldom-seen problem message18.5.3.2 =

    if (made_match == FALSE) {
        THIS_IS_AN_ORDINARY_PROBLEM;
        Problems::quote_source(1, current_sentence);
        Problems::quote_wording(2, Node::get_text(p));
        Problems::quote_kind(3, K);
        Problems::quote_kind(4, domain);
        StandardProblems::handmade_problem(Task::syntax_tree(), _p_(BelievedImpossible));
        Problems::issue_problem_segment(
            "In the line %1, the text '%2' seems to be a description of %3, but "
            "a description of %4 was required.");
        Problems::issue_problem_end();
        return NEVER_MATCH;
    }

§18.5.3.3. I can't see an easy proof that this can never occur, but nor can I make it happen. The problem message is just in case someone finds a way. It appears if the description has a proposition with other than one free variable, once any universal quantifier ("all", etc.) is removed.

Issue a problem message for a malformed proposition in the description18.5.3.3 =

    THIS_IS_AN_ORDINARY_PROBLEM;
    Problems::quote_source(1, current_sentence);
    Problems::quote_wording(2, Node::get_text(p));
    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(BelievedImpossible));
    Problems::issue_problem_segment(
        "In the line %1, the text '%2' is given where a description of a collection "
        "of things or values was required. For instance, 'rooms which contain "
        "something', or 'closed containers' - note that there is no need to say "
        "'all' or 'every' in this context, as that is understood already.");
    Problems::issue_problem_end();
    return NEVER_MATCH;

§18.5.4. It might look as if this ought to be checked when phrase definitions are made; the trouble is, "action", "condition" and so on are valid in phrase definitions, but only in inline-defined ones. We don't want to get into all that here, because the message is aimed more at Inform novices who have made an understandable confusion.

Step (5.d.4) Reject plausible but wrong uses due to use of inline-only types in phrases18.5.4 =

    if (Lvalues::is_lvalue(p)) {
        kind *K = Specifications::to_kind(p);
        if (K == NULL) {
            THIS_IS_AN_ORDINARY_PROBLEM;
            Problems::quote_source(1, current_sentence);
            Problems::quote_wording(2, Node::get_text(p));
            StandardProblems::handmade_problem(Task::syntax_tree(), _p_(BelievedImpossible));  screened out at definition time
            Problems::issue_problem_segment(
                "In the line %1, '%2' ought to be a value, but isn't - there must be "
                "something fishy about the way it was created. %P"
                "Usually this happens because it is one of the named items in "
                "a phrase definition, but stood for a chunk of text which can't "
                "be a value - for instance, 'To marvel at (feat - an action)' "
                "doesn't make 'feat' a value. (Calling it a 'stored action' "
                "would have been fine; and similarly, if you want something "
                "which is either true or false, use 'truth state' not 'condition'.)");
            Problems::issue_problem_end();
            return NEVER_MATCH;
        }
    }

§18.6. Rule (5.e). The "main rule" is, as we shall see, that p should have the same species as expected, or if expected give no species then at least it should have the same family. The two exceptional cases are when expected is a description such as "an even number", or the name of a kind of value such as "a scene", in which case we allow p if it's a value which meets these requirements.

Step (5.e) The Main Rule of Type-Checking18.6 =

    int exceptional_case = FALSE;
    Step (5.e.2) Exception: when expecting a generic or actual CONSTANT18.6.1;
    if (exceptional_case == FALSE) Step (5.e.3) Main rule18.6.2;

§18.6.1. Now for the related, but slightly simpler, case of matching the name of a kind. Suppose we are parsing "award 5 points" against

To award (N - a number) points: ...

Here p will be the actual constant value 5, and expected the generic constant value with kind "number".

A phrase which returns a value must have its own return value's kind checked. Unfortunately we can't do that yet: we want to wait until recursive type-checking has removed incorrect invocations before drawing a conclusion about the return kind of the phrase.

Step (5.e.2) Exception: when expecting a generic or actual CONSTANT18.6.1 =

    LOG_DASH("(5.e.2)");
    if ((kind_expected) && (Specifications::is_value(p))) {
        if (Node::is(p, PHRASE_TO_DECIDE_VALUE_NT)) {
            LOGIF(MATCHING, "(5.e.2) exempting phrase from return value checking for now\n");
        } else {
            switch (Kinds::compatible(
                Specifications::to_kind(p),
                kind_expected)) {
                case NEVER_MATCH:
                    Fail with a mismatched value problem message18.6.1.1;
                case SOMETIMES_MATCH:
                    outcome = SOMETIMES_MATCH;
                    LOGIF(MATCHING, "dropping to sometimes level\n");
                    return outcome;
                    break;
                case ALWAYS_MATCH: break;
            }
        }
        exceptional_case = TRUE;
    }

§18.6.1.1. This is the error message a typical C compiler's type-checker would issue; it says the value has the wrong kind.

Fail with a mismatched value problem message18.6.1.1 =

    THIS_IS_AN_ORDINARY_PROBLEM;
    LOG("Offending subtree: $T\n", p);
    Problems::quote_source(1, current_sentence);
    Problems::quote_wording(2, Node::get_text(p));
    Problems::quote_kind(3, kind_expected);

    if (Node::is(p, LOCAL_VARIABLE_NT)) {
        local_variable *lvar = Node::get_constant_local_variable(p);
        Problems::quote_kind(4, LocalVariables::kind(lvar));
        StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_LocalMismatch));
        Problems::issue_problem_segment(
            "You wrote %1, but '%2' is a temporary name for %4 (created by 'let' "
            "or 'repeat'), whereas I was expecting to find %3 there.");
        Problems::issue_problem_end();
    } else if (Kinds::eq(kind_expected, K_sayable_value)) {
        Problems::quote_kind(4, Specifications::to_kind(p));
        StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_AllSayInvsFailed));
        if (Wordings::empty(Node::get_text(p)))
            Problems::issue_problem_segment(
                "You wrote %1, but that only works for sayable values, that is, "
                "values which I can display in text form. '%2' isn't one of those "
                "values: it's %4, a kind which isn't sayable.");
        else
            Problems::issue_problem_segment(
                "You wrote %1, but that only works for sayable values, that is, "
                "values which I can display in text form. This isn't one of those "
                "values: it's %4, a kind which isn't sayable.");
        Problems::issue_problem_end();
    } else {
        LOG("Found: %u; Expected: %u\n", Specifications::to_kind(p),
            kind_expected);
        Problems::quote_kind(4, Specifications::to_kind(p));
        StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_TypeMismatch));
        if (Wordings::empty(Node::get_text(p)))
            Problems::issue_problem_segment(
                "You wrote %1, but that has the wrong kind of value: %4 rather than %3.");
        else
            Problems::issue_problem_segment(
                "You wrote %1, but '%2' has the wrong kind of value: %4 rather than %3.");
        Problems::issue_problem_end();
    }
    return NEVER_MATCH;

§18.6.2. We now apply the main rule, supposing that neither of the exceptional cases has intervened to stop us getting here. The found and expected specifications must have the same family and, unless the expected species is UNKNOWN_NT, the same species as well.

Step (5.e.3) Main rule18.6.2 =

    LOG_DASH("(5.e.3)");
    if ((kind_expected) || (condition_context)) {
        int condition_found = FALSE;
        if (Specifications::is_condition(p)) condition_found = TRUE;
        if (condition_found != condition_context) {
            if ((Specifications::is_description(p)) && (kind_expected))
                Fail with a warning about literal descriptions18.6.2.1
            else
                Fail with a catch-all typechecking problem message18.6.2.2;
        }
    }

§18.6.2.1. Fail with a warning about literal descriptions18.6.2.1 =

    if (Descriptions::is_complex(p)) {
        THIS_IS_AN_INTERESTING_PROBLEM {
            Problems::quote_source(1, current_sentence);
            Problems::quote_wording(2, Node::get_text(p));
            Problems::quote_kind(3, kind_expected);
            Problems::quote_kind_of(4, p);
            StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_GenericDescription));
            Problems::issue_problem_segment(
                "You wrote %1, but '%2' is used in a context where I'd expect to see "
                "a (single) specific example of %3. Although what you wrote did "
                "make sense as a description, it could refer to many different "
                "values or to none, so it wasn't specific enough.");
            Problems::issue_problem_end();
        }
    } else {
        THIS_IS_AN_INTERESTING_PROBLEM {
            Problems::quote_source(1, current_sentence);
            Problems::quote_wording(2, Node::get_text(p));
            Problems::quote_kind(3, kind_expected);
            Problems::quote_kind_of(4, p);
            StandardProblems::handmade_problem(Task::syntax_tree(), _p_(PM_LiteralDescriptionAsValue));
            Problems::issue_problem_segment(
                "You wrote %1, but '%2' is used in a context where I'd expect to see "
                "a (single) specific example of %3, not a description.");
            if ((Specifications::is_kind_like(p)) &&
                (Kinds::eq(Specifications::to_kind(p), K_time)))
                Problems::issue_problem_segment(
                    " %P(If you meant the current time, this is called 'time "
                    "of day' in Inform to avoid confusing it with the various "
                    "other meanings that the word 'time' can have.)");
            Problems::issue_problem_end();
        }
    }
    return NEVER_MATCH;

§18.6.2.2. This is the general-purpose Problem message to which the type-checker resorts when it has nothing more specific to say.

Fail with a catch-all typechecking problem message18.6.2.2 =

    THIS_IS_AN_ORDINARY_PROBLEM;

    Problems::quote_source(1, current_sentence);
    Problems::quote_wording(2, Node::get_text(p));
    Problems::quote_kind(3, kind_expected);
    Problems::quote_kind_of(4, p);
    StandardProblems::handmade_problem(Task::syntax_tree(), _p_(BelievedImpossible));  at any rate I haven't seen it lately
    Problems::issue_problem_segment(
        "You wrote %1, but '%2' seems to be %4, whereas I was expecting to "
        "find %3 there.");
    Problems::issue_problem_end();
    return NEVER_MATCH;

§21. Ambiguity testing flags. To avoid filling the parse tree with unnecessary annotations, we apply these only when resolving ambiguities, in (4A) above.

define PASSED_DASHFLAG                 0x00000001  once type-checked: did this pass type checking?
define UNPROVEN_DASHFLAG               0x00000002  once type-checked: will this need run-time checking?
define GROSSLY_FAILED_DASHFLAG         0x00000004  once type-checked: oh, this one failed big time
define TESTED_DASHFLAG                 0x00000008  has been type-checked
define INTERESTINGLY_FAILED_DASHFLAG   0x00000010  an interesting problem message could be produced about the way this failed
int Dash::reading_passed(parse_node *p) {
    if (Dash::test_flag(p, PASSED_DASHFLAG)) return TRUE;
    else if (Dash::test_flag(p, TESTED_DASHFLAG)) return FALSE;
    return NOT_APPLICABLE;
}

char *Dash::verdict_to_text(parse_node *p) {
    if (p == NULL) return "(no node)";
    char *verdict = "untested";
    if (Dash::test_flag(p, TESTED_DASHFLAG))               verdict = "failed";
    if (Dash::test_flag(p, INTERESTINGLY_FAILED_DASHFLAG)) verdict = "interesting";
    if (Dash::test_flag(p, GROSSLY_FAILED_DASHFLAG))       verdict = "gross";
    if (Dash::test_flag(p, PASSED_DASHFLAG))               verdict = "proven";
    if (Dash::test_flag(p, UNPROVEN_DASHFLAG))             verdict = "unproven";
    return verdict;
}

char *Dash::quick_verdict_to_text(parse_node *p) {
    if (p == NULL) return "?";
    char *verdict = "-";
    if (Dash::test_flag(p, TESTED_DASHFLAG))               verdict = "f";
    if (Dash::test_flag(p, INTERESTINGLY_FAILED_DASHFLAG)) verdict = "i";
    if (Dash::test_flag(p, GROSSLY_FAILED_DASHFLAG))       verdict = "g";
    if (Dash::test_flag(p, PASSED_DASHFLAG))               verdict = "p";
    if (Dash::test_flag(p, UNPROVEN_DASHFLAG))             verdict = "u";
    return verdict;
}

§22. The bitmap holding the results of typechecking:

void Dash::set_flag(parse_node *p, int flag) {
    if (p == NULL) internal_error("tried to set flag for null p");
    int bm = Annotations::read_int(p, epistemological_status_ANNOT);
    Annotations::write_int(p, epistemological_status_ANNOT, bm | flag);
}

void Dash::clear_flags(parse_node *p) {
    if (p == NULL) internal_error("tried to clear flags for null p");
    Annotations::write_int(p, epistemological_status_ANNOT, 0);
}

void Dash::clear_flag(parse_node *p, int flag) {
    if (p == NULL) internal_error("tried to clear flag for null p");
    int bm = Annotations::read_int(p, epistemological_status_ANNOT);
    Annotations::write_int(p, epistemological_status_ANNOT, bm & (~flag));
}

int Dash::test_flag(parse_node *p, int flag) {
    int bm = (p)?(Annotations::read_int(p, epistemological_status_ANNOT)):0;
    if (bm & flag) return TRUE;
    return FALSE;
}

§23. A convenience sometimes needed for checking conditional clauses, like the "when..." attached to action patterns:

int Dash::validate_conditional_clause(parse_node *spec) {
    if (spec == NULL) return TRUE;
    if (Node::is(spec, UNKNOWN_NT)) return FALSE;
    if (Dash::check_condition(spec) == NEVER_MATCH) return FALSE;
    return TRUE;
}

§24. The exceptional treatment of the "property" kind below is to allow "examining scenery" to be an action pattern, where an either/or property has a name which is really a noun rather than an adjective, luring people into treating it as such.

parse_node *last_spec_failing_to_validate = NULL;
kind *last_kind_failing_to_validate = NULL;
kind *last_kind_found_failing_to_validate = NULL;

void Dash::clear_validation_case(void) {
    last_spec_failing_to_validate = NULL;
    last_kind_failing_to_validate = NULL;
    last_kind_found_failing_to_validate = NULL;
}

int Dash::get_validation_case(parse_node **spec, kind **set_K,
    kind **set_K2) {
    *spec = last_spec_failing_to_validate;
    *set_K = last_kind_failing_to_validate;
    *set_K2 = last_kind_found_failing_to_validate;
    if ((*spec == NULL) || (*set_K == NULL)) return FALSE;
    return TRUE;
}

int Dash::validate_parameter(parse_node *spec, kind *K) {
    parse_node *vts;
    kind *kind_found = NULL;
    if (spec == NULL) return TRUE;
    if (Node::is(spec, UNKNOWN_NT)) goto DontValidate;

    if (Specifications::is_description(spec)) {
        pcalc_prop *prop = Descriptions::to_proposition(spec);
        if ((prop) && (Binding::number_free(prop) != 1)) return FALSE;
    }

    if (Specifications::is_description(spec)) Dash::check_condition(spec);
    else Dash::check_value(spec, NULL);  to force a generic return kind to be evaluated
    kind_found = Specifications::to_kind(spec);
    if ((Kinds::get_construct(kind_found) == CON_property) && (Kinds::Behaviour::is_object(K)))
        return TRUE;
    if ((K_understanding) && (Kinds::eq(kind_found, K_snippet)) &&
        (Kinds::eq(K, K_understanding)))
        return TRUE;
    if ((K_understanding) && (Kinds::eq(K, K_understanding)) &&
        (Node::is(spec, CONSTANT_NT) == FALSE) &&
        (Kinds::eq(kind_found, K_text)))
        goto DontValidate;
    vts = Specifications::from_kind(K);
    if (Dash::compatible_with_description(spec, vts) == NEVER_MATCH) {
        if ((K_understanding) && (Kinds::eq(K, K_understanding)) && (Node::is(spec, CONSTANT_NT))) {
            vts = Specifications::from_kind(K_snippet);
            if (Dash::compatible_with_description(spec, vts) != NEVER_MATCH) return TRUE;
        }
        if (Kinds::eq(kind_found, K_value)) return TRUE;  pick up later in type-checking
        goto DontValidate;
    }
    return TRUE;

    DontValidate:
        last_spec_failing_to_validate = Node::duplicate(spec);
        last_kind_failing_to_validate = K;
        last_kind_found_failing_to_validate = kind_found;
        return FALSE;
}

§25. This is the state of the *** pseudo-phrase used for debugging:

int verbose_checking_state = FALSE;
linked_list *packages_to_log_inter_from = NULL;

void Dash::tracing_phrases(inchar32_t *text) {
    if ((text) && (text[0])) {
        TEMPORARY_TEXT(LT)
        WRITE_TO(LT, "%w", text);
        if (Str::eq_insensitive(LT, I"inter")) {
            inter_package *pack = Functions::package_being_compiled();
            if (pack) {
                if (packages_to_log_inter_from == NULL)
                    packages_to_log_inter_from = NEW_LINKED_LIST(inter_package);
                ADD_TO_LINKED_LIST(pack, inter_package, packages_to_log_inter_from);
            }
        } else {
            Log::set_aspect_from_command_line(LT, FALSE);
        }
        DISCARD_TEXT(LT)
        verbose_checking_state = TRUE;
    } else {
        verbose_checking_state = (verbose_checking_state)?FALSE:TRUE;
        if (verbose_checking_state == FALSE) {
            Log::set_all_aspects(FALSE);
        } else {
            Log::set_aspect(MATCHING_DA, TRUE);
            Log::set_aspect(KIND_CHECKING_DA, TRUE);
            Log::set_aspect(LOCAL_VARIABLES_DA, TRUE);
        }
    }
}

linked_list *Dash::phrases_to_log(void) {
    return packages_to_log_inter_from;
}

§26. Value checking. The following adapts the above test to attempt to match two specifications together: for example, to match "12" against "even number". This, rather surprisingly, returns SOMETIMES_MATCH, since we find that the kinds are guaranteed — 12 is indeed a number — but Inform doesn't "know" the meaning of the word "even", only that it's a test which will be applied at run time.

int Dash::compatible_with_description(parse_node *from_spec, parse_node *to_spec) {
    LOGIF(KIND_CHECKING, "[Can we match from: $P to: $P?]\n", from_spec, to_spec);

    kind *from = Specifications::to_kind(from_spec);
    kind *to = Specifications::to_kind(to_spec);

    int result = NEVER_MATCH;
    if ((from) && (to)) result = Kinds::compatible(from, to);
    else if (to) result = SOMETIMES_MATCH;

    if ((Descriptions::is_qualified(to_spec)) || (Descriptions::to_instance(to_spec)))
        result = Dash::worst_case(result, SOMETIMES_MATCH);

    switch(result) {
        case ALWAYS_MATCH:    LOGIF(KIND_CHECKING, "[Always]\n"); break;
        case SOMETIMES_MATCH: LOGIF(KIND_CHECKING, "[Sometimes]\n"); break;
        case NEVER_MATCH:     LOGIF(KIND_CHECKING, "[Never]\n"); break;
    }
    return result;
}

§27. Ambiguous alternatives.

define AMBIGUITY_JOIN_SYNTAX_CALLBACK Dash::ambiguity_join
int Dash::ambiguity_join(parse_node *existing, parse_node *reading) {
    if ((Specifications::is_phrasal(reading)) &&
        (Node::get_type(reading) == Node::get_type(existing))) {
        Dash::add_pr_inv(existing, reading);
        return TRUE;
    }
    return FALSE;
}

void Dash::add_pr_inv(parse_node *E, parse_node *reading) {
    for (parse_node *N = reading->down->down, *next_N = (N)?(N->next_alternative):NULL; N;
        N = next_N, next_N = (N)?(N->next_alternative):NULL)
        Dash::add_single_pr_inv(E, N);
}

void Dash::add_single_pr_inv(parse_node *E, parse_node *N) {
    E = E->down->down;
    if (Invocations::same_phrase_and_tokens(E, N)) return;
    while ((E) && (E->next_alternative)) {
        E = E->next_alternative;
        if (Invocations::same_phrase_and_tokens(E, N)) return;
    }
    E->next_alternative = N; N->next_alternative = NULL;
}

§28. Internal testing.

void Dash::perform_dash_internal_test(OUTPUT_STREAM, struct internal_test_case *itc) {
    int full = FALSE;
    wording W = itc->text_supplying_the_case;
    Perform a Dash internal test28.1;
}

void Dash::perform_dashlog_internal_test(OUTPUT_STREAM, struct internal_test_case *itc) {
    int full = TRUE;
    wording W = itc->text_supplying_the_case;
    Perform a Dash internal test28.1;
}

§28.1. Perform a Dash internal test28.1 =

    kind *K = NULL;
    parse_node *test_tree = NULL, *last_alt = NULL;
    <s-value-uncached>->multiplicitous = TRUE;
    <s-value-uncached>->ins.watched = TRUE;
    int n = 0;
    while (Wordings::nonempty(W)) {
        wording T = W;
        if (<phrase-with-comma-notation>(W)) {
            T = GET_RW(<phrase-with-comma-notation>, 1);
            W = GET_RW(<phrase-with-comma-notation>, 2);
        } else W = EMPTY_WORDING;
        if (<k-kind>(T)) K = <<rp>>;
        else if (<s-value-uncached>(T)) {
            parse_node *p = <<rp>>;
            if (last_alt) last_alt->next_alternative = p;
            else test_tree = p;
            last_alt = p;
            n++;
        } else LOG("Failed to parse: %W\n", T);
    }
    <s-value-uncached>->multiplicitous = FALSE;
    <s-value-uncached>->ins.watched = FALSE;
    if (n > 1) {
        parse_node *holder = Node::new(AMBIGUITY_NT);
        holder->down = test_tree;
        test_tree = holder;
    }
    LOG("$m\n", test_tree);
    if (K) {
        LOG("Dash: value of kind %u\n", K);
        if (full) Dash::tracing_phrases(NULL);
        int rv = Dash::check_value(test_tree, K);
        char *trv = "ALWAYS";
        if (rv == SOMETIMES_MATCH) trv = "SOMETIMES";
        if (rv == NEVER_MATCH) trv = "NEVER";
        LOG("Result: %s\n", trv);
        if (full) Dash::tracing_phrases(NULL);
        LOG("$m\n", test_tree);
    }