Booking lists are linked lists of rule bookings. The content of a rulebook is a booking list.


§1. Introduction. Bookings are intended to be bound together in linked lists, each of which represents the interior pages of a single rulebook.

typedef struct booking_list {
    struct booking *list_head;  the dummy entry at the front
    CLASS_DEFINITION
} booking_list;

§2. There are only three operations on lists: creation, addition of a booking, and removal of a booking. The following invariants are preserved:

§3. These macros are useful for iterating through the contents in sequence; note that br is never equal to the dummy head, and that pr is never NULL — though it is initially equal to the dummy, which of course is the point of having the dummy.

define LOOP_OVER_BOOKINGS(br, L)
    for (booking *br=(L)?(L->list_head->next_booking):NULL; br; br=br->next_booking)
define LOOP_OVER_BOOKINGS_WITH_PREV(br, pr, L)
    for (booking *br = L->list_head->next_booking, *pr = L->list_head; br;
        pr = br, br = br->next_booking)
void BookingLists::log(booking_list *L) {
    if (L == NULL) { LOG("<null-booked-rule-list>\n"); return; }
    int t = BookingLists::length(L);
    if (t == 0) { LOG("<empty-booked-rule-list>\n"); return; }
    int s = 1;
    LOOP_OVER_BOOKINGS(br, L)
        LOG("  %d/%d. $b\n", s++, t, br);
}

§4. Creation. A new list is a dummy header (see (a) above):

booking_list *BookingLists::new(void) {
    booking_list *L = CREATE(booking_list);
    L->list_head = RuleBookings::new(NULL);
    return L;
}

§5. Addition. The following is called when a booking br for the rule R needs to be placed into list L, which is the contents of a rulebook we will call B. This happens, for example, in response to an an assertion like "R is listed after S in B"; in that cast the side would be AFTER_SIDE, and the ref_rule would be S.

It is also possible to specify a placing. For example, for a rule described as "First every turn rule: ...", this would be added to the every turn rulebook with placing FIRST_PLACEMENT. There are three possible placings: see booking for what they are.

And since there are four possible sides, the following function effectively has 12 different modes to get right.

Each "addition" leaves the list either the same size or longer by 1.

define BEFORE_SIDE -1  before the reference rule
define IN_SIDE 0       if no mention is made of where to put the new booking
define AFTER_SIDE 1    after the reference rule
define INSTEAD_SIDE 2  in place of the reference rule
void BookingLists::add(booking_list *L, booking *new_br,
    int placing, int side, rule *ref_rule) {
    Make some sanity checks on the addition instructions5.1;
    Handle the case where the new rule is already in the list5.2;
    Handle all placements made with the INSTEAD side5.3;
    Handle all placements made with the FIRST placement5.4;
    Handle all placements made with the LAST placement5.5;
    Handle what's left: MIDDLE placements on the IN, BEFORE or AFTER sides5.6;
}

§5.1. Make some sanity checks on the addition instructions5.1 =

    if ((side != IN_SIDE) && (ref_rule == NULL))
        internal_error("tried to add before or after or instead of non-rule");
    if ((side == IN_SIDE) && (ref_rule != NULL))
        internal_error("tried to add in middle but with ref rule");
    if ((side != IN_SIDE) && (placing != MIDDLE_PLACEMENT))
        internal_error("tried to add before or after but with non-middle placement");
    if (L == NULL)
        internal_error("tried to add rule to null list");
    switch(placing) {
        case MIDDLE_PLACEMENT: break;
        case FIRST_PLACEMENT: LOGIF(RULE_ATTACHMENTS, "Placed first\n"); break;
        case LAST_PLACEMENT: LOGIF(RULE_ATTACHMENTS, "Placed last\n"); break;
        default: internal_error("invalid placing of rule");
    }

§5.2. If R is already in B, the assertion may still have an effect.

If our instructions specify no particular position for R to take, we return because nothing need be done: the rule's there already, so be happy. Otherwise, we remove R's existing booking in order that it can be rebooked in a new position.

This is a change in semantics from the original Inform 7 design for rulebooks, under which a rule could be booked multiple times in the same rulebook — which was then called "duplication". Following debate on the Usenet newsgroup rec.arts.int-fiction in February and March 2009, it was decided to abolish duplication in favour of the clean principle that a rule can only be in a single rulebook once. This makes it easier to place rules with tricky preambles, though there were arguments on both sides.

Handle the case where the new rule is already in the list5.2 =

    LOOP_OVER_BOOKINGS_WITH_PREV(pos, prev, L)
        if (Rules::eq(RuleBookings::get_rule(pos), RuleBookings::get_rule(new_br))) {
            if ((side == IN_SIDE) && (placing == MIDDLE_PLACEMENT)) return;
            prev->next_booking = pos->next_booking;
            pos->next_booking = NULL;
            LOGIF(RULE_ATTACHMENTS, "Removing previous entry from rulebook\n");
            break;  rule can only appear once, so no need to keep checking
        }

§5.3. Handle all placements made with the INSTEAD side5.3 =

    if (side == INSTEAD_SIDE) {
        LOOP_OVER_BOOKINGS_WITH_PREV(pos, prev, L)
            if (Rules::eq(RuleBookings::get_rule(pos), ref_rule)) {
                new_br->placement = pos->placement;  replace with same placement
                new_br->next_booking = pos->next_booking;
                prev->next_booking = new_br;
            }
        return;
    }

§5.4. If we insert a rule as first-placed rule when there already is a first-placed rule, the new one displaces it to go first, but both continue to be labelled as having FIRST_PLACEMENT, so that subsequent rule insertions of middle-placed rules will still go after both of them.

Handle all placements made with the FIRST placement5.4 =

    if (placing == FIRST_PLACEMENT) {  first in valid interval (must be whole list)
        booking *previously_first = L->list_head->next_booking;
        L->list_head->next_booking = new_br;
        new_br->next_booking = previously_first;  pushes any existing first rule forward
        new_br->placement = placing;
        return;
    }

§5.5. Symmetrically, a second last-placed rule is inserted after any existing one, but both are labelled as having LAST_PLACEMENT.

Handle all placements made with the LAST placement5.5 =

    if (placing == LAST_PLACEMENT) {  last in valid interval (must be whole list)
        booking *prev = L->list_head;
        while (prev->next_booking != NULL) prev = prev->next_booking;
        prev->next_booking = new_br;  pushes any existing last rule backward
        new_br->next_booking = NULL;
        new_br->placement = placing;
        return;
    }

§5.6. Handle what's left: MIDDLE placements on the IN, BEFORE or AFTER sides5.6 =

    booking *start_rule = L->list_head;  valid interval begins after this rule
    booking *end_rule = NULL;  valid interval ends before this rule, or runs to end if NULL
    Adjust the valid interval to take care of BEFORE and AFTER side requirements5.6.1;
    Check that the valid interval is indeed as advertised5.6.2;
    booking *insert_after = start_rule;  insertion point is after this
    Find insertion point, keeping the valid interval in specificity order5.6.3;
    booking *subseq = insert_after->next_booking;
    insert_after->next_booking = new_br;
    new_br->next_booking = subseq;
    Set the placement for the new rule booking5.6.4;

§5.6.1. Adjust the valid interval to take care of BEFORE and AFTER side requirements5.6.1 =

    switch(side) {
        case BEFORE_SIDE:
            LOOP_OVER_BOOKINGS(pos, L)
                if (Rules::eq(RuleBookings::get_rule(pos), ref_rule))
                    end_rule = pos;  insert before: so valid interval ends here
            if (end_rule == NULL) internal_error("can't find end rule");
            break;
        case AFTER_SIDE:
            LOOP_OVER_BOOKINGS(pos, L)
                if (Rules::eq(RuleBookings::get_rule(pos), ref_rule))
                    start_rule = pos;  insert after: so valid interval begins here
            if (start_rule == L->list_head) internal_error("can't find start rule");
            break;
    }

§5.6.2. Check that the valid interval is indeed as advertised5.6.2 =

    int i = 0, t = 2;
    if (end_rule == NULL) t = 1;
    booking *pos;
    for (pos = L->list_head; pos; pos = pos->next_booking) {
        if ((pos == start_rule) && (i == 0)) i = 1;
        if ((pos == end_rule) && (i == 1)) i = 2;
    }
    if (i != t) internal_error("valid rule interval isn't");

§5.6.3. Find insertion point, keeping the valid interval in specificity order5.6.3 =

    int log = FALSE; if (Log::aspect_switched_on(SPECIFICITIES_DA)) log = TRUE;

     move forward to final valid first rule (if any exist)
    while ((insert_after->next_booking != end_rule)
        && (insert_after->next_booking->placement == FIRST_PLACEMENT))
        insert_after = insert_after->next_booking;

     move forward past other middle rules if they are not less specific
    while ((insert_after->next_booking != end_rule)  stop before \(p\) leaves valid range
        && (insert_after->next_booking->placement != LAST_PLACEMENT)  or reaches a last rule
        && (RuleBookings::cmp(insert_after->next_booking, new_br,
            log) >= 0))  or a rule less specific than the new one
        insert_after = insert_after->next_booking;

§5.6.4. Since this part of the algorithm is used only when we've requested MIDDLE placement, it might seem that new_br->placement should always be set to that. This does indeed mostly happen, but not always. To preserve rulebook invariants (b) and (c), we need to force anything added after a LAST rule to be LAST as well, and similarly for FIRSTs. (This will only happen in cases where the source text called for placements AFTER a LAST rule, or BEFORE a FIRST one.)

Set the placement for the new rule booking5.6.4 =

    new_br->placement = MIDDLE_PLACEMENT;
    if ((insert_after != L->list_head) &&
        (insert_after->placement == LAST_PLACEMENT))
        new_br->placement =
            LAST_PLACEMENT;  happens if valid interval is after a last rule

    if ((subseq) &&
        (subseq->placement == FIRST_PLACEMENT))
        new_br->placement =
            FIRST_PLACEMENT;  happens if valid interval is before a first rule

§6. Removal. This is much simpler, since it doesn't disturb the ordering:

void BookingLists::remove(booking_list *L, rule *R) {
    LOOP_OVER_BOOKINGS_WITH_PREV(br, pr, L)
        if (Rules::eq(RuleBookings::get_rule(br), R)) {
            pr->next_booking = br->next_booking;
            return;
        }
}

§7. Scanning lists for their contents.

int BookingLists::length(booking_list *L) {
    int n = 0;
    LOOP_OVER_BOOKINGS(br, L) n++;
    return n;
}

booking *BookingLists::first(booking_list *L) {
    LOOP_OVER_BOOKINGS(br, L) return br;
    return NULL;
}

int BookingLists::is_empty_of_i7_rules(booking_list *L) {
    LOOP_OVER_BOOKINGS(br, L)
        if (Rules::get_imperative_definition(RuleBookings::get_rule(br)))
            return FALSE;
    return TRUE;
}

int BookingLists::contains(booking_list *L, rule *to_find) {
    LOOP_OVER_BOOKINGS(br, L)
        if (Rules::eq(RuleBookings::get_rule(br), to_find))
            return TRUE;
    return FALSE;
}

int BookingLists::contains_ph(booking_list *L, id_body *idb_to_find) {
    LOOP_OVER_BOOKINGS(br, L) {
        imperative_defn *id = Rules::get_imperative_definition(RuleBookings::get_rule(br));
        if ((id) && (id->body_of_defn == idb_to_find))
            return TRUE;
    }
    return FALSE;
}