To create and subsequently parse against the list of phrase options with which the user can choose to invoke a To phrase.


§1. Introduction. A "phrase option" is a sort of modifier tacked on to an invocation of a phrase; the only modifiers allowed are those declared in that phrase's preamble. Phrase options are an early feature of Inform 7 going back to a time when its priority was to enable the paraphrasing of Inform 6 library features (such as the bitmap passed as a parameter to the list-printer).

I now slightly regret the existence of phrase options, but above all the comma-based syntax used for them, as here. Brackets would have been better; it makes phrase options impossible to use for text substitutions.

let R be the best route from X to Y, using doors;

I sometimes even regret the existence of phrase options, but it must be admitted that they are a clean way to interface to low-level Inform 6 code. But it's mostly the comma which annoys me (making text substitutions unable to support phrase options); I should have gone for brackets.

The id_options_data for an imperative definition, which is part of its type data, says what options it allows:

define MAX_OPTIONS_PER_PHRASE 16  because held in a 16-bit Z-machine bitmap
typedef struct id_options_data {
    struct phrase_option *options_permitted[MAX_OPTIONS_PER_PHRASE];
    int no_options_permitted;
    struct wording options_declaration;  the text declaring the whole set of options
    int multiple_options_permitted;  can be combined, or mutually exclusive?
} id_options_data;

typedef struct phrase_option {
    struct wording name;  text of name
} phrase_option;

§2. Creation. By default, a phrase has no options.

id_options_data PhraseOptions::new(wording W) {
    id_options_data phod;
    phod.no_options_permitted = 0;
    phod.multiple_options_permitted = FALSE;
    phod.options_declaration = W;
    return phod;
}

int PhraseOptions::allows_options(id_body *idb) {
    id_options_data *phod = &(idb->type_data.options_data);
    if (phod->no_options_permitted > 0) return TRUE;
    return FALSE;
}

§3.

int PM_TooManyPhraseOptions_issued = FALSE;
void PhraseOptions::phod_add_phrase_option(id_options_data *phod, wording W) {
    LOGIF(PHRASE_CREATIONS, "Adding phrase option <%W>\n", W);
    if (phod->no_options_permitted >= MAX_OPTIONS_PER_PHRASE) {
        if (PM_TooManyPhraseOptions_issued == FALSE)
            StandardProblems::sentence_problem(Task::syntax_tree(),
                _p_(PM_TooManyPhraseOptions),
                "a phrase is only allowed to have 16 different options",
                "so either some of these will need to go, or you may want to consider "
                "breaking up the phrase into simpler ones whose usage is easier to describe.");
        PM_TooManyPhraseOptions_issued = TRUE;
        return;
    }
    PM_TooManyPhraseOptions_issued = FALSE;  so the problem can recur on later phrases

    phrase_option *po = CREATE(phrase_option);
    po->name = W;
    phod->options_permitted[phod->no_options_permitted++] = po;
}

§4. Parsing. This isn't very efficient, but doesn't need to be, since phrase options are parsed only in a condition context, not in a value context, and these are relatively rare in Inform source text.

id_options_data *phod_being_parsed = NULL;
id_body *idb_being_parsed = NULL;

int PhraseOptions::parse_phod(id_options_data *phod, wording W) {
    for (int i = 0; i < phod->no_options_permitted; i++)
        if (Wordings::match(W, phod->options_permitted[i]->name))
            return (1 << i);
    return -1;
}
int PhraseOptions::parse(id_body *idb, wording W) {
    return PhraseOptions::parse_phod(&(idb->type_data.options_data), W);
}

§5. Which we wrap up thus:

<phrase-option> internal {
    int bitmap = PhraseOptions::parse_phod(phod_being_parsed, W);
    if (bitmap == -1) { ==> { fail nonterminal }; }
    ==> { bitmap, - };
    return TRUE;
}

§6. Parsing phrase options in a declaration. The following is called with W set to just the part of a phrase prototype containing its phrase options. In this example:

To decide which object is best route from (R1 - object) to (R2 - object),
    using doors or using even locked doors:

W would be "using doors or using even locked doors".

The syntax is just a list of names, but with the wrinkle that if the list is divided with "or" then the options are mutually exclusive, but with "and/or" they're not.

void PhraseOptions::parse_declared_options(id_options_data *phod, wording W) {
    if (Wordings::nonempty(W)) {
        phod->options_declaration = W;
        phod_being_parsed = phod;
        <phrase-option-decl-list>(W);
        if (<<r>>) phod->multiple_options_permitted = TRUE;
    }
}

§7. Note the following Preform grammar passes the return value TRUE up from the final element of the list when the connective used for it was "and/or". Note also the rare use of the Preform literal marker in \and/or to show that the slash between "and" and "or" is part of the word.

<phrase-option-decl-list> ::=
    ... |                                                           ==> { lookahead }
    <phrase-option-decl-setting-entry> <phrase-option-decl-tail> |  ==> { pass 2 }
    <phrase-option-decl-setting-entry>                              ==> { FALSE, - }

<phrase-option-decl-tail> ::=
    , _or <phrase-option-decl-list> |                               ==> { pass 1 }
    , \and/or <phrase-option-decl-list> |                           ==> { TRUE, - }
    _,/or <phrase-option-decl-list> |                               ==> { pass 1 }
    \and/or <phrase-option-decl-list>                               ==> { TRUE, - }

<phrase-option-decl-setting-entry> ::=
    ... |                                                           ==> { lookahead }
    ...                                                             ==> Add a phrase option7.1;

§7.1. Add a phrase option7.1 =

    PhraseOptions::phod_add_phrase_option(phod_being_parsed, W);
    ==> { FALSE, - };

§8. Parsing phrase options in an invocation. At this point, we're looking at the text after the first comma in something like:

list the contents of the box, as a sentence, with newlines;

The invocation has already been parsed enough that we know the options chosen are:

as a sentence, with newlines

and the following routine turns that into a bitmap with two bits set, one corresponding to each choice.

We return TRUE or FALSE according to whether the options were valid or not, and the silently flag suppresses problem messages we would otherwise produce.

int phod_being_parsed_silently = FALSE;  context for the grammar below

int PhraseOptions::parse_invoked_options(parse_node *inv, int silently) {
    id_body *idb = Node::get_phrase_invoked(inv);
    wording W = Invocations::get_phrase_options(inv);

    idb_being_parsed = idb;
    phod_being_parsed = &(idb_being_parsed->type_data.options_data);

    int bitmap = 0;
    int pc = problem_count;
    Parse the supplied list of options into a bitmap8.1;

    Invocations::set_phrase_options_bitmap(inv, bitmap);
    if (problem_count > pc) return FALSE;
    return TRUE;
}

§8.1. Parse the supplied list of options into a bitmap8.1 =

    int s = phod_being_parsed_silently;
    phod_being_parsed_silently = silently;
    if (<phrase-option-list>(W)) bitmap = <<r>>;
    phod_being_parsed_silently = s;

    if ((problem_count == pc) &&
        (phod_being_parsed->multiple_options_permitted == FALSE))
        Reject this if multiple options are set8.1.1;

§8.1.1. Ah, bit-twiddling: fun for all the family. There's no point computing the population count of the bitmap, that is, the number of bits set: we only need to know if it's a power of 2 or not. Note that subtracting 1, in binary, clears the least significant set bit, leaves the higher bits as they are, and changes the lower bits (which were previously all 0s) to 1s. So taking a bitwise-and of a number and itself minus one leaves just the higher bits alone. The original number therefore had a single set bit if and only if this residue is zero.

Reject this if multiple options are set8.1.1 =

    if ((bitmap & (bitmap - 1)) != 0) {
        if (silently == FALSE) {
            Problems::quote_source(1, current_sentence);
            Problems::quote_wording(2, W);
            Problems::quote_phrase(3, idb);
            Problems::quote_wording(4, phod_being_parsed->options_declaration);
            StandardProblems::handmade_problem(Task::syntax_tree(),
                _p_(PM_PhraseOptionsExclusive));
            Problems::issue_problem_segment(
                "You wrote %1, supplying the options '%2' to the phrase '%3', but "
                "the options listed for this phrase ('%4') are mutually exclusive.");
            Problems::issue_problem_end();
        }
        return FALSE;
    }

§9. When setting options, in an actual use of a phrase, the list is divided by "and":

<phrase-option-list> ::=
    ... |                                                ==> { lookahead }
    <phrase-option-setting-entry> <phrase-option-tail> | ==> { R[1] | R[2], - }
    <phrase-option-setting-entry>                        ==> { pass 1 }

<phrase-option-tail> ::=
    , _and <phrase-option-list> |                        ==> { pass 1 }
    _,/and <phrase-option-list>                          ==> { pass 1 }

<phrase-option-setting-entry> ::=
    <phrase-option> |                                    ==> { pass 1 }
    ...  ==> Issue PM_NotAPhraseOption or C22NotTheOnlyPhraseOption problem9.1

§9.1. Issue PM_NotAPhraseOption or C22NotTheOnlyPhraseOption problem9.1 =

    if ((!preform_lookahead_mode) && (!phod_being_parsed_silently)) {
        Problems::quote_source(1, current_sentence);
        Problems::quote_wording(2, W);
        Problems::quote_phrase(3, idb_being_parsed);
        Problems::quote_wording(4, phod_being_parsed->options_declaration);
        if (phod_being_parsed->no_options_permitted > 1) {
            StandardProblems::handmade_problem(Task::syntax_tree(),
                _p_(PM_NotAPhraseOption));
            Problems::issue_problem_segment(
                "You wrote %1, but '%2' is not one of the options allowed on "
                "the end of the phrase '%3'. (The options allowed are: '%4'.)");
            Problems::issue_problem_end();
        } else {
            StandardProblems::handmade_problem(Task::syntax_tree(),
                _p_(PM_NotTheOnlyPhraseOption));
            Problems::issue_problem_segment(
                "You wrote %1, but the only option allowed on the end of the "
                "phrase '%3' is '%4', so '%2' is not something I know how to "
                "deal with.");
            Problems::issue_problem_end();
        }
    }